diff --git a/pyproject.toml b/pyproject.toml
index 974c0b3..a7d12af 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -12,15 +12,16 @@ authors = [
 ]
 readme = "README.md"
 license = {text = "GPL-3.0"}
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 dependencies = [
+    "aiohttp",  # Explicit dependency until compatibility with async-timeout is resolved
     "click>=8.0.0",
     "zigpy>=0.70.0",
     "crc",
     "bellows>=0.42.0",
     'gpiod; platform_system=="Linux"',
     "coloredlogs",
-    "async_timeout",
+    'async-timeout; python_version<"3.11"',
     "typing_extensions",
     "pyserial-asyncio-fast",
 ]
diff --git a/universal_silabs_flasher/common.py b/universal_silabs_flasher/common.py
index 938fcde..3a7e988 100644
--- a/universal_silabs_flasher/common.py
+++ b/universal_silabs_flasher/common.py
@@ -7,13 +7,18 @@
 import functools
 import logging
 import re
+import sys
 import typing
 
-import async_timeout
 import click
 import crc
 import zigpy.serial
 
+if sys.version_info[:2] < (3, 11):
+    from async_timeout import timeout as asyncio_timeout  # pragma: no cover
+else:
+    from asyncio import timeout as asyncio_timeout  # pragma: no cover
+
 if typing.TYPE_CHECKING:
     from typing_extensions import Self
 
@@ -121,7 +126,7 @@ async def wait_for_state(self, state: str) -> None:
 async def connect_protocol(port, baudrate, factory):
     loop = asyncio.get_running_loop()
 
-    async with async_timeout.timeout(CONNECT_TIMEOUT):
+    async with asyncio_timeout(CONNECT_TIMEOUT):
         _, protocol = await zigpy.serial.create_serial_connection(
             loop=loop,
             protocol_factory=factory,
diff --git a/universal_silabs_flasher/cpc.py b/universal_silabs_flasher/cpc.py
index ebb4e29..f65047f 100644
--- a/universal_silabs_flasher/cpc.py
+++ b/universal_silabs_flasher/cpc.py
@@ -5,12 +5,11 @@
 import logging
 import typing
 
-import async_timeout
 from zigpy.serial import SerialProtocol
 import zigpy.types
 
 from . import cpc_types
-from .common import BufferTooShort, Version, crc16_ccitt
+from .common import BufferTooShort, Version, asyncio_timeout, crc16_ccitt
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -354,7 +353,7 @@ async def send_unnumbered_frame(
                 self.send_data(frame.serialize())
 
                 try:
-                    async with async_timeout.timeout(timeout):
+                    async with asyncio_timeout(timeout):
                         return await asyncio.shield(future)
                 except asyncio.TimeoutError:
                     _LOGGER.debug(
diff --git a/universal_silabs_flasher/flasher.py b/universal_silabs_flasher/flasher.py
index 7dd4e84..bb7906e 100644
--- a/universal_silabs_flasher/flasher.py
+++ b/universal_silabs_flasher/flasher.py
@@ -5,13 +5,18 @@
 import logging
 import typing
 
-import async_timeout
 import bellows.config
 import bellows.ezsp
 import bellows.types
 from zigpy.serial import SerialProtocol
 
-from .common import PROBE_TIMEOUT, Version, connect_protocol, pad_to_multiple
+from .common import (
+    PROBE_TIMEOUT,
+    Version,
+    asyncio_timeout,
+    connect_protocol,
+    pad_to_multiple,
+)
 from .const import DEFAULT_BAUDRATES, GPIO_CONFIGS, ApplicationType, ResetTarget
 from .cpc import CPCProtocol
 from .emberznet import connect_ezsp
@@ -244,11 +249,11 @@ async def enter_bootloader(self) -> None:
             pass
         elif self.app_type is ApplicationType.CPC:
             async with self._connect_cpc(self.app_baudrate) as cpc:
-                async with async_timeout.timeout(PROBE_TIMEOUT):
+                async with asyncio_timeout(PROBE_TIMEOUT):
                     await cpc.enter_bootloader()
         elif self.app_type is ApplicationType.SPINEL:
             async with self._connect_spinel(self.app_baudrate) as spinel:
-                async with async_timeout.timeout(PROBE_TIMEOUT):
+                async with asyncio_timeout(PROBE_TIMEOUT):
                     await spinel.enter_bootloader()
         elif self.app_type is ApplicationType.EZSP:
             async with self._connect_ezsp(self.app_baudrate) as ezsp:
diff --git a/universal_silabs_flasher/gecko_bootloader.py b/universal_silabs_flasher/gecko_bootloader.py
index d423933..87e5133 100644
--- a/universal_silabs_flasher/gecko_bootloader.py
+++ b/universal_silabs_flasher/gecko_bootloader.py
@@ -6,10 +6,9 @@
 import re
 import typing
 
-import async_timeout
 from zigpy.serial import SerialProtocol
 
-from .common import PROBE_TIMEOUT, StateMachine, Version
+from .common import PROBE_TIMEOUT, StateMachine, Version, asyncio_timeout
 from .xmodemcrc import send_xmodem128_crc
 
 _LOGGER = logging.getLogger(__name__)
@@ -68,7 +67,7 @@ def __init__(self) -> None:
 
     async def probe(self) -> Version:
         """Attempt to communicate with the bootloader."""
-        async with async_timeout.timeout(PROBE_TIMEOUT):
+        async with asyncio_timeout(PROBE_TIMEOUT):
             return await self.ebl_info()
 
     async def ebl_info(self) -> Version:
@@ -93,7 +92,7 @@ async def run_firmware(self) -> None:
         self.send_data(GeckoBootloaderOption.RUN_FIRMWARE)
 
         try:
-            async with async_timeout.timeout(RUN_APPLICATION_DELAY):
+            async with asyncio_timeout(RUN_APPLICATION_DELAY):
                 await self._state_machine.wait_for_state(State.IN_MENU)
         except asyncio.TimeoutError:
             # The menu did not appear so the application must be running
@@ -134,7 +133,7 @@ async def upload_firmware(
 
         # The menu is sometimes sent immediately after upload
         try:
-            async with async_timeout.timeout(MENU_AFTER_UPLOAD_TIMEOUT):
+            async with asyncio_timeout(MENU_AFTER_UPLOAD_TIMEOUT):
                 await self._state_machine.wait_for_state(State.IN_MENU)
         except asyncio.TimeoutError:
             # If not, trigger it manually
diff --git a/universal_silabs_flasher/spinel.py b/universal_silabs_flasher/spinel.py
index c5ce53c..9b0656b 100644
--- a/universal_silabs_flasher/spinel.py
+++ b/universal_silabs_flasher/spinel.py
@@ -5,11 +5,10 @@
 import logging
 import typing
 
-import async_timeout
 from zigpy.serial import SerialProtocol
 import zigpy.types
 
-from .common import Version, crc16_kermit
+from .common import Version, asyncio_timeout, crc16_kermit
 from .spinel_types import CommandID, HDLCSpecial, PropertyID, ResetReason
 
 _LOGGER = logging.getLogger(__name__)
@@ -210,7 +209,7 @@ async def send_frame(
                 self.send_data(HDLCLiteFrame(data=new_frame.serialize()).serialize())
 
                 try:
-                    async with async_timeout.timeout(timeout):
+                    async with asyncio_timeout(timeout):
                         return await asyncio.shield(future)
                 except asyncio.TimeoutError:
                     _LOGGER.debug(
diff --git a/universal_silabs_flasher/xmodemcrc.py b/universal_silabs_flasher/xmodemcrc.py
index 145a6e0..f20f648 100644
--- a/universal_silabs_flasher/xmodemcrc.py
+++ b/universal_silabs_flasher/xmodemcrc.py
@@ -5,10 +5,9 @@
 import logging
 import typing
 
-import async_timeout
 import zigpy.types
 
-from .common import crc16_ccitt
+from .common import asyncio_timeout, crc16_ccitt
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -66,7 +65,7 @@ async def send_xmodem128_crc_data(
         await writer.drain()
 
         # And wait for a response
-        async with async_timeout.timeout(RECEIVE_TIMEOUT):
+        async with asyncio_timeout(RECEIVE_TIMEOUT):
             rsp_byte = await reader.readexactly(1)
 
         _LOGGER.debug("Got response: %r", rsp_byte)