Repository: python-telegram-bot/python-telegram-bot
Pull request: Use `timedelta` to represent time periods in classes
Second part of #4575.

<details>
<summary>Affected classes (basicaly anywhere "in seconds" appears in api docs)</summary>

| Class | attribute | Type | docs |
| ---- | ----------- | ----- | ----- |
| ChatFullInfo | slow_mode_delay | Integer | Optional. For supergroups, the minimum allowed delay between consecutive messages sent by each unprivileged user; in seconds |
| ChatFullInfo | message_auto_delete_time | Integer | Optional. The time after which all messages sent to the chat will be automatically deleted; in seconds |
| Animation | duration | Integer | Duration of the video in seconds as defined by the sender |
| Audio | duration | Integer | Duration of the audio in seconds as defined by the sender |
| Video | duration | Integer | Duration of the video in seconds as defined by the sender |
| Video | start_timestamp | Integer | Optional. Timestamp in seconds from which the video will play in the message |
| VideoNote | duration | Integer | Duration of the video in seconds as defined by the sender |
| Voice | duration | Integer | Duration of the audio in seconds as defined by the sender |
| PaidMediaPreview | duration | Integer | Optional. Duration of the media in seconds as defined by the sender |
| Poll | open_period | Integer | Optional. Amount of time in seconds the poll will be active after creation |
| Location | live_period | Integer | Optional. Time relative to the message sending date, during which the location can be updated; in seconds. For active live locations only. |
| MessageAutoDeleteTimerChanged | message_auto_delete_time | Integer | New auto-delete time for messages in the chat; in seconds |
| VideoChatEnded | duration | Integer | Video chat duration in seconds |
| ChatInviteLink | subscription_period | Integer | Optional. The number of seconds the subscription will be active for before the next payment |
| ResponseParameters | retry_after | Integer | Optional. In case of exceeding flood control, the number of seconds left to wait before the request can be repeated |
| InputMediaVideo | duration | Integer | Optional. Video duration in seconds |
| InputMediaAnimation | duration | Integer | Optional. Animation duration in seconds |
| InputMediaAudio | duration | Integer | Optional. Duration of the audio in seconds |
| InputPaidMediaVideo | duration | Integer | Optional. Video duration in seconds |
| InlineQueryResultGif | gif_duration | Integer | Optional. Duration of the GIF in seconds |
| InlineQueryResultMpeg4Gif | mpeg4_duration | Integer | Optional. Video duration in seconds |
| InlineQueryResultVideo | video_duration | Integer | Optional. Video duration in seconds |
| InlineQueryResultAudio | audio_duration | Integer | Optional. Audio duration in seconds |
| InlineQueryResultVoice | voice_duration | Integer | Optional. Recording duration in seconds |
| InlineQueryResultLocation | live_period | Integer | Optional. Period in seconds during which the location can be updated, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely. |
| InputLocationMessageContent | live_period | Integer | Optional. Period in seconds during which the location can be updated, should be between 60 and 86400, or 0x7FFFFFFF for live locations that can be edited indefinitely. |
</details>


Check-list for PRs
------------------

- [x] Added ``.. versionadded:: NEXT.VERSION``, ``.. versionchanged:: NEXT.VERSION``, ``.. deprecated:: NEXT.VERSION`` or ``.. versionremoved:: NEXT.VERSION`` to the docstrings for user facing changes (for methods/class descriptions, arguments and attributes)
- [x] Created new or adapted existing unit tests
- [ ] Documented code changes according to the `CSI standard <https://standards.mousepawmedia.com/en/stable/csi.html>`__
- [x] Checked the `Stability Policy <https://docs.python-telegram-bot.org/stability_policy.html>`_ in case of deprecations or changes to documented behavior

**If the PR contains API changes (otherwise, you can ignore this passage)**

- [ ] Checked the Bot API specific sections of the `Stability Policy <https://docs.python-telegram-bot.org/stability_policy.html>`_
- [ ] Created a PR to remove functionality deprecated in the previous Bot API release (`see here <https://docs.python-telegram-bot.org/en/stable/stability_policy.html#case-2>`_)


-  If relevant:
   - [x] Added or updated documentation for the changed class(es) and/or method(s)


Problem statement:
Use `datetime.timedelta` to Represent Time Periods
### What kind of feature are you missing? Where do you notice a shortcoming of PTB?

Several Telegram Objects and methods currently contain fields/parameters with names `\w+_period`, wherer the integer value represents a time span in seconds.
Working with plain seconds is not very pythonic, just like working with posix timestamps isn't. 

### Describe the solution you'd like

In the long run, we should convert the attributes to `timedelta` objects and make the parameters accept timedelta objects.

#### Methods Parameters

This is realtively easy, simply accept `timedelta` as well as integers and pass `totalseconds()` to TG. Converting `dtm.timedelta` to `int` can probably be done in the internals of `Bot` or requests so that it doesn't have to happen in each bot method.

#### Classes

* Both attributes and arguments should *only* accept timedeltas in the long run.
* Since this is a breaking change, we should issue a warning for now about this changing in the future and encouraging users to harden their code against the change
* For making the `timedelta` support available early, I have two ideas, ~both optional~
  * ~We can provide a temporary property that returns the timedelta object (e.g. `xy_period_timedelta`). This would need to issue a warning stating that it will be removed once `timedelta` is natively supported. Not overly clean …~
  * We anyways need to make the `*_period` attributes properties to issue the deprecation warning. We could use this and look for an environment variable, say `PTB_TIMEDELTA`. If set, we return the timedelta object, otherwise the `int`. → let's do this

### Describe alternatives you've considered

_No response_

### Additional context

Adding support for method parameters can be done independently of handling classes.

Hints from issue discussion:
Hi, my class group and I want to work in this Issue.

Group:
@Jeam-zx
@Migueldsc12
@AnyaMarcanito
@henryg311
@cuevasrja 
@cuevasrja & friends if you're still interested, would you like to get started on this? I think it would be good to split this into two PRs, one for the Bot method arguments and one for the classes. Some additional pointers on both:

* for the arguments of Bot methods, converting `timedelta` to `int` should happen at
    https://github.com/python-telegram-bot/python-telegram-bot/blob/2ac52018c29c791aa0438750e5bcdf2da8945bf8/telegram/request/_requestparameter.py#L116-L117
    and corresponding new parametrizations need to be added to
    https://github.com/python-telegram-bot/python-telegram-bot/blob/2ac52018c29c791aa0438750e5bcdf2da8945bf8/tests/request/test_requestparameter.py#L98-L101
    Moreover, it would be good to explicitly tests timedelta inputs for the arguments known so far. I expect that the corresponding test functions can be decorated with something like `@pytest.mark.parametrize("foo_period", [42, dtm.timedelta(seconds=42)])` so that no additional test functions are needed.
* for converting arguments of classes to `timedelta`, please add a new helper function to 
    https://github.com/python-telegram-bot/python-telegram-bot/blob/2ac52018c29c791aa0438750e5bcdf2da8945bf8/telegram/_utils/argumentparsing.py#L19
    It should directly issue deprecation warnings if the input is an integer. Search for `PTBDeprecationWarning` in the code base to see some example on how we do that. We'll also want unit tests that ensure that the warning is issued correctly for all casses that use the conversion
* converting the `int` value received from telegram to timedelta should happen in the `de_json` method of the respective class. If the class doesn't override `TelegramObject.de_json` yet, add that. The `test_de_json` unit tests for the respective classes will have to be updated
* for the attributes of classes: Let's say we consider `subscription_period`. Change `self.subscription_period = …` to `self._subscription_period = …` and add a property `subscription_period`. Add a new helper function to
    https://github.com/python-telegram-bot/python-telegram-bot/blob/2ac52018c29c791aa0438750e5bcdf2da8945bf8/telegram/_utils/datetime.py#L19
    along the lines
    ```python
    def get_timedelta_value(value: dtm.timedelta) -> Union[int, dtm.timedelta]:
        if os.get_env("PTB_TIMEDELTA"):
            return value
        # issue PTBDeprecationWarning explaining that in the future dtm.timedelta will be returned and encouraging users to set the env var
        return value.totalseconds()
    ```
    Here as well, we'll need unit tests that ensure that the properties return the expected value with/without the env var and issue the warning correctly
Hi @Bibo-Joshi , Sorry, we are coming back from vacation and now I am reading your message.

Yes, not all of us will be able to participate but we are still interested in contributing to the project if possible even if it is not for an academic reason.
No worries :) glad to hear that you'd like to contribute. In the meantime I've already created #4651 for the bot method part, but the class parameter/attribute part is still open. You're welcome to open a PR for that. Let me know if you have any questions.
@cuevasrja how is it looking for your? Ideally, I'd still like to get this into a 21.x release, such that we can remove any deprecation shims in 23.0

Commit message: Fix precommit.

Failure at `src/telegram/_utils/datetime.py:38:0` is not related to this
PR. Not sure why it's being reported just now (CI only). It's not caught
locally (with fresh precommit cache).

Patch to review:
diff --git a/changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml b/changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml
new file mode 100644
index 00000000000..ae225ae88d3
--- /dev/null
+++ b/changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml
@@ -0,0 +1,36 @@
+features = "Use `timedelta` to represent time periods in classes"
+deprecations = """In this release, we're migrating attributes of Telegram objects that represent durations/time periods from having :obj:`int` type to Python's native :class:`datetime.timedelta`.
+
+Set ``PTB_TIMEDELTA=true`` as an environment variable to make these attributes return :obj:`datetime.timedelta` objects instead of integers. Support for :obj:`int` values is deprecated and will be removed in a future major version.
+
+Affected Attributes:
+- :attr:`telegram.ChatFullInfo.slow_mode_delay` and :attr:`telegram.ChatFullInfo.message_auto_delete_time`
+- :attr:`telegram.Animation.duration`
+- :attr:`telegram.Audio.duration`
+- :attr:`telegram.Video.duration` and :attr:`telegram.Video.start_timestamp`
+- :attr:`telegram.VideoNote.duration`
+- :attr:`telegram.Voice.duration`
+- :attr:`telegram.PaidMediaPreview.duration`
+- :attr:`telegram.VideoChatEnded.duration`
+- :attr:`telegram.InputMediaVideo.duration`
+- :attr:`telegram.InputMediaAnimation.duration`
+- :attr:`telegram.InputMediaAudio.duration`
+- :attr:`telegram.InputPaidMediaVideo.duration`
+- :attr:`telegram.InlineQueryResultGif.gif_duration`
+- :attr:`telegram.InlineQueryResultMpeg4Gif.mpeg4_duration`
+- :attr:`telegram.InlineQueryResultVideo.video_duration`
+- :attr:`telegram.InlineQueryResultAudio.audio_duration`
+- :attr:`telegram.InlineQueryResultVoice.voice_duration`
+- :attr:`telegram.InlineQueryResultLocation.live_period`
+- :attr:`telegram.Poll.open_period`
+- :attr:`telegram.Location.live_period`
+- :attr:`telegram.MessageAutoDeleteTimerChanged.message_auto_delete_time`
+- :attr:`telegram.ChatInviteLink.subscription_period`
+- :attr:`telegram.InputLocationMessageContent.live_period`
+- :attr:`telegram.error.RetryAfter.retry_after`
+"""
+internal = "Modify `test_official` to handle time periods as timedelta automatically."
+[[pull_requests]]
+uid = "4750"
+author_uid = "aelkheir"
+closes_threads = ["4575"]
diff --git a/docs/substitutions/global.rst b/docs/substitutions/global.rst
index 8fb9e9360d7..88cf095caa3 100644
--- a/docs/substitutions/global.rst
+++ b/docs/substitutions/global.rst
@@ -101,3 +101,5 @@
 .. |org-verify| replace:: `on behalf of the organization <https://telegram.org/verify#third-party-verification>`__
 
 .. |time-period-input| replace:: :class:`datetime.timedelta` objects are accepted in addition to plain :obj:`int` values.
+
+.. |time-period-int-deprecated| replace:: In a future major version this attribute will be of type :obj:`datetime.timedelta`. You can opt-in early by setting `PTB_TIMEDELTA=true` as an environment variable.
diff --git a/examples/rawapibot.py b/examples/rawapibot.py
index b6a70fc3de0..34ac964c4b7 100644
--- a/examples/rawapibot.py
+++ b/examples/rawapibot.py
@@ -7,6 +7,7 @@
 """
 import asyncio
 import contextlib
+import datetime as dtm
 import logging
 from typing import NoReturn
 
@@ -47,7 +48,9 @@ async def main() -> NoReturn:
 async def echo(bot: Bot, update_id: int) -> int:
     """Echo the message the user sent."""
     # Request updates after the last update_id
-    updates = await bot.get_updates(offset=update_id, timeout=10, allowed_updates=Update.ALL_TYPES)
+    updates = await bot.get_updates(
+        offset=update_id, timeout=dtm.timedelta(seconds=10), allowed_updates=Update.ALL_TYPES
+    )
     for update in updates:
         next_update_id = update.update_id + 1
 
diff --git a/src/telegram/_bot.py b/src/telegram/_bot.py
index 90f6cf0bf42..5f588f430a8 100644
--- a/src/telegram/_bot.py
+++ b/src/telegram/_bot.py
@@ -4519,7 +4519,7 @@ async def get_updates(
         self,
         offset: Optional[int] = None,
         limit: Optional[int] = None,
-        timeout: Optional[int] = None,
+        timeout: Optional[TimePeriod] = None,
         allowed_updates: Optional[Sequence[str]] = None,
         *,
         read_timeout: ODVInput[float] = DEFAULT_NONE,
@@ -4554,9 +4554,12 @@ async def get_updates(
                 between :tg-const:`telegram.constants.PollingLimit.MIN_LIMIT`-
                 :tg-const:`telegram.constants.PollingLimit.MAX_LIMIT` are accepted.
                 Defaults to ``100``.
-            timeout (:obj:`int`, optional): Timeout in seconds for long polling. Defaults to ``0``,
-                i.e. usual short polling. Should be positive, short polling should be used for
-                testing purposes only.
+            timeout (:obj:`int` | :class:`datetime.timedelta`, optional): Timeout in seconds for
+                long polling. Defaults to ``0``, i.e. usual short polling. Should be positive,
+                short polling should be used for testing purposes only.
+
+                .. versionchanged:: NEXT.VERSION
+                    |time-period-input|
             allowed_updates (Sequence[:obj:`str`]), optional): A sequence the types of
                 updates you want your bot to receive. For example, specify ["message",
                 "edited_channel_post", "callback_query"] to only receive updates of these types.
@@ -4591,6 +4594,12 @@ async def get_updates(
         else:
             arg_read_timeout = self._request[0].read_timeout or 0
 
+        read_timeout = (
+            (arg_read_timeout + timeout.total_seconds())
+            if isinstance(timeout, dtm.timedelta)
+            else (arg_read_timeout + timeout if timeout else arg_read_timeout)
+        )
+
         # Ideally we'd use an aggressive read timeout for the polling. However,
         # * Short polling should return within 2 seconds.
         # * Long polling poses a different problem: the connection might have been dropped while
@@ -4601,7 +4610,7 @@ async def get_updates(
             await self._post(
                 "getUpdates",
                 data,
-                read_timeout=arg_read_timeout + timeout if timeout else arg_read_timeout,
+                read_timeout=read_timeout,
                 write_timeout=write_timeout,
                 connect_timeout=connect_timeout,
                 pool_timeout=pool_timeout,
diff --git a/src/telegram/_chatfullinfo.py b/src/telegram/_chatfullinfo.py
index 4b0fae53c6b..4a313c296f6 100644
--- a/src/telegram/_chatfullinfo.py
+++ b/src/telegram/_chatfullinfo.py
@@ -20,7 +20,7 @@
 """This module contains an object that represents a Telegram ChatFullInfo."""
 import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._birthdate import Birthdate
 from telegram._chat import Chat, _ChatBase
@@ -29,9 +29,18 @@
 from telegram._files.chatphoto import ChatPhoto
 from telegram._gifts import AcceptedGiftTypes
 from telegram._reaction import ReactionType
-from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
-from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import (
+    de_json_optional,
+    de_list_optional,
+    parse_period_arg,
+    parse_sequence_arg,
+)
+from telegram._utils.datetime import (
+    extract_tzinfo_from_defaults,
+    from_timestamp,
+    get_timedelta_value,
+)
+from telegram._utils.types import JSONDict, TimePeriod
 from telegram._utils.warnings import warn
 from telegram._utils.warnings_transition import (
     build_deprecation_warning_message,
@@ -166,17 +175,23 @@ class ChatFullInfo(_ChatBase):
             (by sending date).
         permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
             for groups and supergroups.
-        slow_mode_delay (:obj:`int`, optional): For supergroups, the minimum allowed delay between
-            consecutive messages sent by each unprivileged user.
+        slow_mode_delay (:obj:`int` | :class:`datetime.timedelta`, optional): For supergroups,
+            the minimum allowed delay between consecutive messages sent by each unprivileged user.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         unrestrict_boost_count (:obj:`int`, optional): For supergroups, the minimum number of
             boosts that a non-administrator user needs to add in order to ignore slow mode and chat
             permissions.
 
             .. versionadded:: 21.0
-        message_auto_delete_time (:obj:`int`, optional): The time after which all messages sent to
-            the chat will be automatically deleted; in seconds.
+        message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`, optional): The time
+            after which all messages sent to the chat will be automatically deleted; in seconds.
 
             .. versionadded:: 13.4
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         has_aggressive_anti_spam_enabled (:obj:`bool`, optional): :obj:`True`, if aggressive
             anti-spam checks are enabled in the supergroup. The field is only available to chat
             administrators.
@@ -331,17 +346,23 @@ class ChatFullInfo(_ChatBase):
             (by sending date).
         permissions (:class:`telegram.ChatPermissions`): Optional. Default chat member permissions,
             for groups and supergroups.
-        slow_mode_delay (:obj:`int`): Optional. For supergroups, the minimum allowed delay between
-            consecutive messages sent by each unprivileged user.
+        slow_mode_delay (:obj:`int` | :class:`datetime.timedelta`): Optional. For supergroups,
+            the minimum allowed delay between consecutive messages sent by each unprivileged user.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         unrestrict_boost_count (:obj:`int`): Optional. For supergroups, the minimum number of
             boosts that a non-administrator user needs to add in order to ignore slow mode and chat
             permissions.
 
             .. versionadded:: 21.0
-        message_auto_delete_time (:obj:`int`): Optional. The time after which all messages sent to
-            the chat will be automatically deleted; in seconds.
+        message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`): Optional. The time
+            after which all messages sent to the chat will be automatically deleted; in seconds.
 
             .. versionadded:: 13.4
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         has_aggressive_anti_spam_enabled (:obj:`bool`): Optional. :obj:`True`, if aggressive
             anti-spam checks are enabled in the supergroup. The field is only available to chat
             administrators.
@@ -383,6 +404,8 @@ class ChatFullInfo(_ChatBase):
 
     __slots__ = (
         "_can_send_gift",
+        "_message_auto_delete_time",
+        "_slow_mode_delay",
         "accent_color_id",
         "accepted_gift_types",
         "active_usernames",
@@ -411,14 +434,12 @@ class ChatFullInfo(_ChatBase):
         "linked_chat_id",
         "location",
         "max_reaction_count",
-        "message_auto_delete_time",
         "permissions",
         "personal_chat",
         "photo",
         "pinned_message",
         "profile_accent_color_id",
         "profile_background_custom_emoji_id",
-        "slow_mode_delay",
         "sticker_set_name",
         "unrestrict_boost_count",
     )
@@ -456,9 +477,9 @@ def __init__(
         invite_link: Optional[str] = None,
         pinned_message: Optional["Message"] = None,
         permissions: Optional[ChatPermissions] = None,
-        slow_mode_delay: Optional[int] = None,
+        slow_mode_delay: Optional[TimePeriod] = None,
         unrestrict_boost_count: Optional[int] = None,
-        message_auto_delete_time: Optional[int] = None,
+        message_auto_delete_time: Optional[TimePeriod] = None,
         has_aggressive_anti_spam_enabled: Optional[bool] = None,
         has_hidden_members: Optional[bool] = None,
         has_protected_content: Optional[bool] = None,
@@ -513,9 +534,9 @@ def __init__(
             self.invite_link: Optional[str] = invite_link
             self.pinned_message: Optional[Message] = pinned_message
             self.permissions: Optional[ChatPermissions] = permissions
-            self.slow_mode_delay: Optional[int] = slow_mode_delay
-            self.message_auto_delete_time: Optional[int] = (
-                int(message_auto_delete_time) if message_auto_delete_time is not None else None
+            self._slow_mode_delay: Optional[dtm.timedelta] = parse_period_arg(slow_mode_delay)
+            self._message_auto_delete_time: Optional[dtm.timedelta] = parse_period_arg(
+                message_auto_delete_time
             )
             self.has_protected_content: Optional[bool] = has_protected_content
             self.has_visible_history: Optional[bool] = has_visible_history
@@ -576,6 +597,16 @@ def can_send_gift(self) -> Optional[bool]:
         )
         return self._can_send_gift
 
+    @property
+    def slow_mode_delay(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._slow_mode_delay, attribute="slow_mode_delay")
+
+    @property
+    def message_auto_delete_time(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(
+            self._message_auto_delete_time, attribute="message_auto_delete_time"
+        )
+
     @classmethod
     def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
         """See :meth:`telegram.TelegramObject.de_json`."""
@@ -600,6 +631,13 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
             Message,
         )
 
+        data["slow_mode_delay"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("slow_mode_delay")) else None
+        )
+        data["message_auto_delete_time"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("message_auto_delete_time")) else None
+        )
+
         data["pinned_message"] = de_json_optional(data.get("pinned_message"), Message, bot)
         data["permissions"] = de_json_optional(data.get("permissions"), ChatPermissions, bot)
         data["location"] = de_json_optional(data.get("location"), ChatLocation, bot)
diff --git a/src/telegram/_chatinvitelink.py b/src/telegram/_chatinvitelink.py
index 289ee48bdba..920e43e85b8 100644
--- a/src/telegram/_chatinvitelink.py
+++ b/src/telegram/_chatinvitelink.py
@@ -18,13 +18,17 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents an invite link for a chat."""
 import datetime as dtm
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._telegramobject import TelegramObject
 from telegram._user import User
-from telegram._utils.argumentparsing import de_json_optional
-from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import de_json_optional, parse_period_arg
+from telegram._utils.datetime import (
+    extract_tzinfo_from_defaults,
+    from_timestamp,
+    get_timedelta_value,
+)
+from telegram._utils.types import JSONDict, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import Bot
@@ -70,10 +74,13 @@ class ChatInviteLink(TelegramObject):
             created using this link.
 
             .. versionadded:: 13.8
-        subscription_period (:obj:`int`, optional): The number of seconds the subscription will be
-            active for before the next payment.
+        subscription_period (:obj:`int` | :class:`datetime.timedelta`, optional): The number of
+            seconds the subscription will be active for before the next payment.
 
             .. versionadded:: 21.5
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         subscription_price (:obj:`int`, optional): The amount of Telegram Stars a user must pay
             initially and after each subsequent subscription period to be a member of the chat
             using the link.
@@ -107,10 +114,13 @@ class ChatInviteLink(TelegramObject):
             created using this link.
 
             .. versionadded:: 13.8
-        subscription_period (:obj:`int`): Optional. The number of seconds the subscription will be
-            active for before the next payment.
+        subscription_period (:obj:`int` | :class:`datetime.timedelta`): Optional. The number of
+            seconds the subscription will be active for before the next payment.
 
             .. versionadded:: 21.5
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         subscription_price (:obj:`int`): Optional. The amount of Telegram Stars a user must pay
             initially and after each subsequent subscription period to be a member of the chat
             using the link.
@@ -120,6 +130,7 @@ class ChatInviteLink(TelegramObject):
     """
 
     __slots__ = (
+        "_subscription_period",
         "creates_join_request",
         "creator",
         "expire_date",
@@ -129,7 +140,6 @@ class ChatInviteLink(TelegramObject):
         "member_limit",
         "name",
         "pending_join_request_count",
-        "subscription_period",
         "subscription_price",
     )
 
@@ -144,7 +154,7 @@ def __init__(
         member_limit: Optional[int] = None,
         name: Optional[str] = None,
         pending_join_request_count: Optional[int] = None,
-        subscription_period: Optional[int] = None,
+        subscription_period: Optional[TimePeriod] = None,
         subscription_price: Optional[int] = None,
         *,
         api_kwargs: Optional[JSONDict] = None,
@@ -164,7 +174,7 @@ def __init__(
         self.pending_join_request_count: Optional[int] = (
             int(pending_join_request_count) if pending_join_request_count is not None else None
         )
-        self.subscription_period: Optional[int] = subscription_period
+        self._subscription_period: Optional[dtm.timedelta] = parse_period_arg(subscription_period)
         self.subscription_price: Optional[int] = subscription_price
 
         self._id_attrs = (
@@ -177,6 +187,10 @@ def __init__(
 
         self._freeze()
 
+    @property
+    def subscription_period(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._subscription_period, attribute="subscription_period")
+
     @classmethod
     def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatInviteLink":
         """See :meth:`telegram.TelegramObject.de_json`."""
@@ -187,5 +201,8 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatInviteLink
 
         data["creator"] = de_json_optional(data.get("creator"), User, bot)
         data["expire_date"] = from_timestamp(data.get("expire_date", None), tzinfo=loc_tzinfo)
+        data["subscription_period"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("subscription_period")) else None
+        )
 
         return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_files/_inputstorycontent.py b/src/telegram/_files/_inputstorycontent.py
index 1eaf14682f3..4481ecf9814 100644
--- a/src/telegram/_files/_inputstorycontent.py
+++ b/src/telegram/_files/_inputstorycontent.py
@@ -25,6 +25,7 @@
 from telegram._files.inputfile import InputFile
 from telegram._telegramobject import TelegramObject
 from telegram._utils import enum
+from telegram._utils.argumentparsing import parse_period_arg
 from telegram._utils.files import parse_file_input
 from telegram._utils.types import FileInput, JSONDict
 
@@ -158,18 +159,8 @@ def __init__(
 
         with self._unfrozen():
             self.video: Union[str, InputFile] = self._parse_file_input(video)
-            self.duration: Optional[dtm.timedelta] = self._parse_period_arg(duration)
-            self.cover_frame_timestamp: Optional[dtm.timedelta] = self._parse_period_arg(
+            self.duration: Optional[dtm.timedelta] = parse_period_arg(duration)
+            self.cover_frame_timestamp: Optional[dtm.timedelta] = parse_period_arg(
                 cover_frame_timestamp
             )
             self.is_animation: Optional[bool] = is_animation
-
-    # This helper is temporarly here until we can use `argumentparsing.parse_period_arg`
-    # from https://github.com/python-telegram-bot/python-telegram-bot/pull/4750
-    @staticmethod
-    def _parse_period_arg(arg: Optional[Union[float, dtm.timedelta]]) -> Optional[dtm.timedelta]:
-        if arg is None:
-            return None
-        if isinstance(arg, dtm.timedelta):
-            return arg
-        return dtm.timedelta(seconds=arg)
diff --git a/src/telegram/_files/animation.py b/src/telegram/_files/animation.py
index 537ffc0a0db..89434a13723 100644
--- a/src/telegram/_files/animation.py
+++ b/src/telegram/_files/animation.py
@@ -17,11 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Animation."""
-from typing import Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._files._basethumbedmedium import _BaseThumbedMedium
 from telegram._files.photosize import PhotoSize
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class Animation(_BaseThumbedMedium):
@@ -41,7 +47,11 @@ class Animation(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         width (:obj:`int`): Video width as defined by the sender.
         height (:obj:`int`): Video height as defined by the sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the video
+            in seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         file_name (:obj:`str`, optional): Original animation filename as defined by the sender.
         mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender.
         file_size (:obj:`int`, optional): File size in bytes.
@@ -58,7 +68,11 @@ class Animation(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         width (:obj:`int`): Video width as defined by the sender.
         height (:obj:`int`): Video height as defined by the sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video in seconds
+            as defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         file_name (:obj:`str`): Optional. Original animation filename as defined by the sender.
         mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender.
         file_size (:obj:`int`): Optional. File size in bytes.
@@ -69,7 +83,7 @@ class Animation(_BaseThumbedMedium):
 
     """
 
-    __slots__ = ("duration", "file_name", "height", "mime_type", "width")
+    __slots__ = ("_duration", "file_name", "height", "mime_type", "width")
 
     def __init__(
         self,
@@ -77,7 +91,7 @@ def __init__(
         file_unique_id: str,
         width: int,
         height: int,
-        duration: int,
+        duration: TimePeriod,
         file_name: Optional[str] = None,
         mime_type: Optional[str] = None,
         file_size: Optional[int] = None,
@@ -96,7 +110,21 @@ def __init__(
             # Required
             self.width: int = width
             self.height: int = height
-            self.duration: int = duration
+            self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
             # Optional
             self.mime_type: Optional[str] = mime_type
             self.file_name: Optional[str] = file_name
+
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Animation":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_files/audio.py b/src/telegram/_files/audio.py
index af5e420e1b2..c0b67b3d2c5 100644
--- a/src/telegram/_files/audio.py
+++ b/src/telegram/_files/audio.py
@@ -17,11 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Audio."""
-from typing import Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._files._basethumbedmedium import _BaseThumbedMedium
 from telegram._files.photosize import PhotoSize
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class Audio(_BaseThumbedMedium):
@@ -39,7 +45,11 @@ class Audio(_BaseThumbedMedium):
             or reuse the file.
         file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
             the same over time and for different bots. Can't be used to download or reuse the file.
-        duration (:obj:`int`): Duration of the audio in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the audio in
+            seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by
             audio tags.
         title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags.
@@ -56,7 +66,11 @@ class Audio(_BaseThumbedMedium):
             or reuse the file.
         file_unique_id (:obj:`str`): Unique identifier for this file, which is supposed to be
             the same over time and for different bots. Can't be used to download or reuse the file.
-        duration (:obj:`int`): Duration of the audio in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the audio in seconds as
+            defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by
             audio tags.
         title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags.
@@ -71,13 +85,13 @@ class Audio(_BaseThumbedMedium):
 
     """
 
-    __slots__ = ("duration", "file_name", "mime_type", "performer", "title")
+    __slots__ = ("_duration", "file_name", "mime_type", "performer", "title")
 
     def __init__(
         self,
         file_id: str,
         file_unique_id: str,
-        duration: int,
+        duration: TimePeriod,
         performer: Optional[str] = None,
         title: Optional[str] = None,
         mime_type: Optional[str] = None,
@@ -96,9 +110,23 @@ def __init__(
         )
         with self._unfrozen():
             # Required
-            self.duration: int = duration
+            self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
             # Optional
             self.performer: Optional[str] = performer
             self.title: Optional[str] = title
             self.mime_type: Optional[str] = mime_type
             self.file_name: Optional[str] = file_name
+
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Audio":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_files/inputmedia.py b/src/telegram/_files/inputmedia.py
index 2b7e6b21fd5..eff69613387 100644
--- a/src/telegram/_files/inputmedia.py
+++ b/src/telegram/_files/inputmedia.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """Base class for Telegram InputMedia Objects."""
+import datetime as dtm
 from collections.abc import Sequence
 from typing import Final, Optional, Union
 
@@ -30,10 +31,11 @@
 from telegram._messageentity import MessageEntity
 from telegram._telegramobject import TelegramObject
 from telegram._utils import enum
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
 from telegram._utils.files import parse_file_input
-from telegram._utils.types import FileInput, JSONDict, ODVInput
+from telegram._utils.types import FileInput, JSONDict, ODVInput, TimePeriod
 from telegram.constants import InputMediaType
 
 MediaType = Union[Animation, Audio, Document, PhotoSize, Video]
@@ -215,7 +217,10 @@ class InputPaidMediaVideo(InputPaidMedia):
             .. versionchanged:: 21.11
         width (:obj:`int`, optional): Video width.
         height (:obj:`int`, optional): Video height.
-        duration (:obj:`int`, optional): Video duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Video duration in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
             suitable for streaming.
 
@@ -233,14 +238,17 @@ class InputPaidMediaVideo(InputPaidMedia):
             .. versionchanged:: 21.11
         width (:obj:`int`): Optional. Video width.
         height (:obj:`int`): Optional. Video height.
-        duration (:obj:`int`): Optional. Video duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Video duration in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is
             suitable for streaming.
     """
 
     __slots__ = (
+        "_duration",
         "cover",
-        "duration",
         "height",
         "start_timestamp",
         "supports_streaming",
@@ -254,7 +262,7 @@ def __init__(
         thumbnail: Optional[FileInput] = None,
         width: Optional[int] = None,
         height: Optional[int] = None,
-        duration: Optional[int] = None,
+        duration: Optional[TimePeriod] = None,
         supports_streaming: Optional[bool] = None,
         cover: Optional[FileInput] = None,
         start_timestamp: Optional[int] = None,
@@ -264,7 +272,7 @@ def __init__(
         if isinstance(media, Video):
             width = width if width is not None else media.width
             height = height if height is not None else media.height
-            duration = duration if duration is not None else media.duration
+            duration = duration if duration is not None else media._duration
             media = media.file_id
         else:
             # We use local_mode=True because we don't have access to the actual setting and want
@@ -278,13 +286,17 @@ def __init__(
             )
             self.width: Optional[int] = width
             self.height: Optional[int] = height
-            self.duration: Optional[int] = duration
+            self._duration: Optional[dtm.timedelta] = parse_period_arg(duration)
             self.supports_streaming: Optional[bool] = supports_streaming
             self.cover: Optional[Union[InputFile, str]] = (
                 parse_file_input(cover, attach=True, local_mode=True) if cover else None
             )
             self.start_timestamp: Optional[int] = start_timestamp
 
+    @property
+    def duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._duration, attribute="duration")
+
 
 class InputMediaAnimation(InputMedia):
     """Represents an animation file (GIF or H.264/MPEG-4 AVC video without sound) to be sent.
@@ -322,7 +334,11 @@ class InputMediaAnimation(InputMedia):
 
         width (:obj:`int`, optional): Animation width.
         height (:obj:`int`, optional): Animation height.
-        duration (:obj:`int`, optional): Animation duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Animation duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the animation needs to be covered
             with a spoiler animation.
 
@@ -350,7 +366,11 @@ class InputMediaAnimation(InputMedia):
                 * |alwaystuple|
         width (:obj:`int`): Optional. Animation width.
         height (:obj:`int`): Optional. Animation height.
-        duration (:obj:`int`): Optional. Animation duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Animation duration
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the animation is covered with a
             spoiler animation.
 
@@ -364,7 +384,7 @@ class InputMediaAnimation(InputMedia):
     """
 
     __slots__ = (
-        "duration",
+        "_duration",
         "has_spoiler",
         "height",
         "show_caption_above_media",
@@ -379,7 +399,7 @@ def __init__(
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         width: Optional[int] = None,
         height: Optional[int] = None,
-        duration: Optional[int] = None,
+        duration: Optional[TimePeriod] = None,
         caption_entities: Optional[Sequence[MessageEntity]] = None,
         filename: Optional[str] = None,
         has_spoiler: Optional[bool] = None,
@@ -391,7 +411,7 @@ def __init__(
         if isinstance(media, Animation):
             width = media.width if width is None else width
             height = media.height if height is None else height
-            duration = media.duration if duration is None else duration
+            duration = duration if duration is not None else media._duration
             media = media.file_id
         else:
             # We use local_mode=True because we don't have access to the actual setting and want
@@ -412,10 +432,14 @@ def __init__(
             )
             self.width: Optional[int] = width
             self.height: Optional[int] = height
-            self.duration: Optional[int] = duration
+            self._duration: Optional[dtm.timedelta] = parse_period_arg(duration)
             self.has_spoiler: Optional[bool] = has_spoiler
             self.show_caption_above_media: Optional[bool] = show_caption_above_media
 
+    @property
+    def duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._duration, attribute="duration")
+
 
 class InputMediaPhoto(InputMedia):
     """Represents a photo to be sent.
@@ -545,7 +569,10 @@ class InputMediaVideo(InputMedia):
 
         width (:obj:`int`, optional): Video width.
         height (:obj:`int`, optional): Video height.
-        duration (:obj:`int`, optional): Video duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Video duration in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         supports_streaming (:obj:`bool`, optional): Pass :obj:`True`, if the uploaded video is
             suitable for streaming.
         has_spoiler (:obj:`bool`, optional): Pass :obj:`True`, if the video needs to be covered
@@ -582,7 +609,10 @@ class InputMediaVideo(InputMedia):
                 * |alwaystuple|
         width (:obj:`int`): Optional. Video width.
         height (:obj:`int`): Optional. Video height.
-        duration (:obj:`int`): Optional. Video duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Video duration in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         supports_streaming (:obj:`bool`): Optional. :obj:`True`, if the uploaded video is
             suitable for streaming.
         has_spoiler (:obj:`bool`): Optional. :obj:`True`, if the video is covered with a
@@ -605,8 +635,8 @@ class InputMediaVideo(InputMedia):
     """
 
     __slots__ = (
+        "_duration",
         "cover",
-        "duration",
         "has_spoiler",
         "height",
         "show_caption_above_media",
@@ -622,7 +652,7 @@ def __init__(
         caption: Optional[str] = None,
         width: Optional[int] = None,
         height: Optional[int] = None,
-        duration: Optional[int] = None,
+        duration: Optional[TimePeriod] = None,
         supports_streaming: Optional[bool] = None,
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence[MessageEntity]] = None,
@@ -638,7 +668,7 @@ def __init__(
         if isinstance(media, Video):
             width = width if width is not None else media.width
             height = height if height is not None else media.height
-            duration = duration if duration is not None else media.duration
+            duration = duration if duration is not None else media._duration
             media = media.file_id
         else:
             # We use local_mode=True because we don't have access to the actual setting and want
@@ -656,7 +686,7 @@ def __init__(
         with self._unfrozen():
             self.width: Optional[int] = width
             self.height: Optional[int] = height
-            self.duration: Optional[int] = duration
+            self._duration: Optional[dtm.timedelta] = parse_period_arg(duration)
             self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input(
                 thumbnail
             )
@@ -668,6 +698,10 @@ def __init__(
             )
             self.start_timestamp: Optional[int] = start_timestamp
 
+    @property
+    def duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._duration, attribute="duration")
+
 
 class InputMediaAudio(InputMedia):
     """Represents an audio file to be treated as music to be sent.
@@ -703,7 +737,11 @@ class InputMediaAudio(InputMedia):
             .. versionchanged:: 20.0
                 |sequenceclassargs|
 
-        duration (:obj:`int`, optional): Duration of the audio in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the audio
+            in seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         performer (:obj:`str`, optional): Performer of the audio as defined by the sender or by
             audio tags.
         title (:obj:`str`, optional): Title of the audio as defined by the sender or by audio tags.
@@ -725,7 +763,11 @@ class InputMediaAudio(InputMedia):
 
                 * |tupleclassattrs|
                 * |alwaystuple|
-        duration (:obj:`int`): Optional. Duration of the audio in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Duration of the audio
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         performer (:obj:`str`): Optional. Performer of the audio as defined by the sender or by
             audio tags.
         title (:obj:`str`): Optional. Title of the audio as defined by the sender or by audio tags.
@@ -735,14 +777,14 @@ class InputMediaAudio(InputMedia):
 
     """
 
-    __slots__ = ("duration", "performer", "thumbnail", "title")
+    __slots__ = ("_duration", "performer", "thumbnail", "title")
 
     def __init__(
         self,
         media: Union[FileInput, Audio],
         caption: Optional[str] = None,
         parse_mode: ODVInput[str] = DEFAULT_NONE,
-        duration: Optional[int] = None,
+        duration: Optional[TimePeriod] = None,
         performer: Optional[str] = None,
         title: Optional[str] = None,
         caption_entities: Optional[Sequence[MessageEntity]] = None,
@@ -752,7 +794,7 @@ def __init__(
         api_kwargs: Optional[JSONDict] = None,
     ):
         if isinstance(media, Audio):
-            duration = media.duration if duration is None else duration
+            duration = duration if duration is not None else media._duration
             performer = media.performer if performer is None else performer
             title = media.title if title is None else title
             media = media.file_id
@@ -773,10 +815,14 @@ def __init__(
             self.thumbnail: Optional[Union[str, InputFile]] = self._parse_thumbnail_input(
                 thumbnail
             )
-            self.duration: Optional[int] = duration
+            self._duration: Optional[dtm.timedelta] = parse_period_arg(duration)
             self.title: Optional[str] = title
             self.performer: Optional[str] = performer
 
+    @property
+    def duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._duration, attribute="duration")
+
 
 class InputMediaDocument(InputMedia):
     """Represents a general file to be sent.
diff --git a/src/telegram/_files/location.py b/src/telegram/_files/location.py
index 87c895b711a..fa6ee7de41c 100644
--- a/src/telegram/_files/location.py
+++ b/src/telegram/_files/location.py
@@ -18,11 +18,17 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Location."""
 
-from typing import Final, Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Final, Optional, Union
 
 from telegram import constants
 from telegram._telegramobject import TelegramObject
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class Location(TelegramObject):
@@ -36,8 +42,12 @@ class Location(TelegramObject):
         latitude (:obj:`float`): Latitude as defined by the sender.
         horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
             measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`, optional): Time relative to the message sending date, during which
-            the location can be updated, in seconds. For active live locations only.
+        live_period (:obj:`int` | :class:`datetime.timedelta`, optional): Time relative to the
+            message sending date, during which the location can be updated, in seconds. For active
+            live locations only.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         heading (:obj:`int`, optional): The direction in which user is moving, in degrees;
             :tg-const:`telegram.Location.MIN_HEADING`-:tg-const:`telegram.Location.MAX_HEADING`.
             For active live locations only.
@@ -49,8 +59,12 @@ class Location(TelegramObject):
         latitude (:obj:`float`): Latitude as defined by the sender.
         horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
             measured in meters; 0-:tg-const:`telegram.Location.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`): Optional. Time relative to the message sending date, during which
-            the location can be updated, in seconds. For active live locations only.
+        live_period (:obj:`int` | :class:`datetime.timedelta`): Optional. Time relative to the
+            message sending date, during which the location can be updated, in seconds. For active
+            live locations only.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         heading (:obj:`int`): Optional. The direction in which user is moving, in degrees;
             :tg-const:`telegram.Location.MIN_HEADING`-:tg-const:`telegram.Location.MAX_HEADING`.
             For active live locations only.
@@ -60,10 +74,10 @@ class Location(TelegramObject):
     """
 
     __slots__ = (
+        "_live_period",
         "heading",
         "horizontal_accuracy",
         "latitude",
-        "live_period",
         "longitude",
         "proximity_alert_radius",
     )
@@ -73,7 +87,7 @@ def __init__(
         longitude: float,
         latitude: float,
         horizontal_accuracy: Optional[float] = None,
-        live_period: Optional[int] = None,
+        live_period: Optional[TimePeriod] = None,
         heading: Optional[int] = None,
         proximity_alert_radius: Optional[int] = None,
         *,
@@ -86,7 +100,7 @@ def __init__(
 
         # Optionals
         self.horizontal_accuracy: Optional[float] = horizontal_accuracy
-        self.live_period: Optional[int] = live_period
+        self._live_period: Optional[dtm.timedelta] = parse_period_arg(live_period)
         self.heading: Optional[int] = heading
         self.proximity_alert_radius: Optional[int] = (
             int(proximity_alert_radius) if proximity_alert_radius else None
@@ -96,6 +110,19 @@ def __init__(
 
         self._freeze()
 
+    @property
+    def live_period(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._live_period, attribute="live_period")
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Location":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        data["live_period"] = dtm.timedelta(seconds=s) if (s := data.get("live_period")) else None
+
+        return super().de_json(data=data, bot=bot)
+
     HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY
     """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`
 
diff --git a/src/telegram/_files/video.py b/src/telegram/_files/video.py
index 36381ebbf6b..357caddaf22 100644
--- a/src/telegram/_files/video.py
+++ b/src/telegram/_files/video.py
@@ -17,13 +17,15 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Video."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._files._basethumbedmedium import _BaseThumbedMedium
 from telegram._files.photosize import PhotoSize
-from telegram._utils.argumentparsing import de_list_optional, parse_sequence_arg
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import de_list_optional, parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import Bot
@@ -46,7 +48,11 @@ class Video(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         width (:obj:`int`): Video width as defined by the sender.
         height (:obj:`int`): Video height as defined by the sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video
+            in seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         file_name (:obj:`str`, optional): Original filename as defined by the sender.
         mime_type (:obj:`str`, optional): MIME type of a file as defined by the sender.
         file_size (:obj:`int`, optional): File size in bytes.
@@ -57,10 +63,13 @@ class Video(_BaseThumbedMedium):
             the video in the message.
 
             .. versionadded:: 21.11
-        start_timestamp (:obj:`int`, optional): Timestamp in seconds from which the video
-            will play in the message
+        start_timestamp (:obj:`int` | :class:`datetime.timedelta`, optional): Timestamp in seconds
+            from which the video will play in the message
             .. versionadded:: 21.11
 
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
+
     Attributes:
         file_id (:obj:`str`): Identifier for this file, which can be used to download
             or reuse the file.
@@ -69,7 +78,11 @@ class Video(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         width (:obj:`int`): Video width as defined by the sender.
         height (:obj:`int`): Video height as defined by the sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video in seconds
+            as defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         file_name (:obj:`str`): Optional. Original filename as defined by the sender.
         mime_type (:obj:`str`): Optional. MIME type of a file as defined by the sender.
         file_size (:obj:`int`): Optional. File size in bytes.
@@ -80,18 +93,21 @@ class Video(_BaseThumbedMedium):
             the video in the message.
 
             .. versionadded:: 21.11
-        start_timestamp (:obj:`int`): Optional, Timestamp in seconds from which the video
-            will play in the message
+        start_timestamp (:obj:`int` | :class:`datetime.timedelta`): Optional. Timestamp in seconds
+            from which the video will play in the message
             .. versionadded:: 21.11
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
     """
 
     __slots__ = (
+        "_duration",
+        "_start_timestamp",
         "cover",
-        "duration",
         "file_name",
         "height",
         "mime_type",
-        "start_timestamp",
         "width",
     )
 
@@ -101,13 +117,13 @@ def __init__(
         file_unique_id: str,
         width: int,
         height: int,
-        duration: int,
+        duration: TimePeriod,
         mime_type: Optional[str] = None,
         file_size: Optional[int] = None,
         file_name: Optional[str] = None,
         thumbnail: Optional[PhotoSize] = None,
         cover: Optional[Sequence[PhotoSize]] = None,
-        start_timestamp: Optional[int] = None,
+        start_timestamp: Optional[TimePeriod] = None,
         *,
         api_kwargs: Optional[JSONDict] = None,
     ):
@@ -122,18 +138,32 @@ def __init__(
             # Required
             self.width: int = width
             self.height: int = height
-            self.duration: int = duration
+            self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
             # Optional
             self.mime_type: Optional[str] = mime_type
             self.file_name: Optional[str] = file_name
             self.cover: Optional[Sequence[PhotoSize]] = parse_sequence_arg(cover)
-            self.start_timestamp: Optional[int] = start_timestamp
+            self._start_timestamp: Optional[dtm.timedelta] = parse_period_arg(start_timestamp)
+
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @property
+    def start_timestamp(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._start_timestamp, attribute="start_timestamp")
 
     @classmethod
     def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Video":
         """See :meth:`telegram.TelegramObject.de_json`."""
         data = cls._parse_data(data)
 
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+        data["start_timestamp"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("start_timestamp")) else None
+        )
         data["cover"] = de_list_optional(data.get("cover"), PhotoSize, bot)
 
         return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_files/videonote.py b/src/telegram/_files/videonote.py
index edb9e555372..1b85fd4b875 100644
--- a/src/telegram/_files/videonote.py
+++ b/src/telegram/_files/videonote.py
@@ -18,11 +18,17 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram VideoNote."""
 
-from typing import Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._files._basethumbedmedium import _BaseThumbedMedium
 from telegram._files.photosize import PhotoSize
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class VideoNote(_BaseThumbedMedium):
@@ -42,7 +48,11 @@ class VideoNote(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         length (:obj:`int`): Video width and height (diameter of the video message) as defined
             by sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video in
+            seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         file_size (:obj:`int`, optional): File size in bytes.
         thumbnail (:class:`telegram.PhotoSize`, optional): Video thumbnail.
 
@@ -56,7 +66,11 @@ class VideoNote(_BaseThumbedMedium):
             Can't be used to download or reuse the file.
         length (:obj:`int`): Video width and height (diameter of the video message) as defined
             by sender.
-        duration (:obj:`int`): Duration of the video in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the video in seconds as
+            defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         file_size (:obj:`int`): Optional. File size in bytes.
         thumbnail (:class:`telegram.PhotoSize`): Optional. Video thumbnail.
 
@@ -64,14 +78,14 @@ class VideoNote(_BaseThumbedMedium):
 
     """
 
-    __slots__ = ("duration", "length")
+    __slots__ = ("_duration", "length")
 
     def __init__(
         self,
         file_id: str,
         file_unique_id: str,
         length: int,
-        duration: int,
+        duration: TimePeriod,
         file_size: Optional[int] = None,
         thumbnail: Optional[PhotoSize] = None,
         *,
@@ -87,4 +101,18 @@ def __init__(
         with self._unfrozen():
             # Required
             self.length: int = length
-            self.duration: int = duration
+            self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
+
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "VideoNote":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_files/voice.py b/src/telegram/_files/voice.py
index 19c0e856d14..b0f2997fbdd 100644
--- a/src/telegram/_files/voice.py
+++ b/src/telegram/_files/voice.py
@@ -17,10 +17,16 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains an object that represents a Telegram Voice."""
-from typing import Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._files._basemedium import _BaseMedium
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class Voice(_BaseMedium):
@@ -35,7 +41,11 @@ class Voice(_BaseMedium):
         file_unique_id (:obj:`str`): Unique identifier for this file, which
             is supposed to be the same over time and for different bots.
             Can't be used to download or reuse the file.
-        duration (:obj:`int`): Duration of the audio in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the audio in
+            seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         mime_type (:obj:`str`, optional): MIME type of the file as defined by the sender.
         file_size (:obj:`int`, optional): File size in bytes.
 
@@ -45,19 +55,23 @@ class Voice(_BaseMedium):
         file_unique_id (:obj:`str`): Unique identifier for this file, which
             is supposed to be the same over time and for different bots.
             Can't be used to download or reuse the file.
-        duration (:obj:`int`): Duration of the audio in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Duration of the audio in seconds as
+            defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         mime_type (:obj:`str`): Optional. MIME type of the file as defined by the sender.
         file_size (:obj:`int`): Optional. File size in bytes.
 
     """
 
-    __slots__ = ("duration", "mime_type")
+    __slots__ = ("_duration", "mime_type")
 
     def __init__(
         self,
         file_id: str,
         file_unique_id: str,
-        duration: int,
+        duration: TimePeriod,
         mime_type: Optional[str] = None,
         file_size: Optional[int] = None,
         *,
@@ -71,6 +85,21 @@ def __init__(
         )
         with self._unfrozen():
             # Required
-            self.duration: int = duration
+            self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
             # Optional
             self.mime_type: Optional[str] = mime_type
+
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Voice":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_inline/inlinequeryresultaudio.py b/src/telegram/_inline/inlinequeryresultaudio.py
index 8e3376a458f..cbfff47470b 100644
--- a/src/telegram/_inline/inlinequeryresultaudio.py
+++ b/src/telegram/_inline/inlinequeryresultaudio.py
@@ -17,15 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultAudio."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
 from telegram._messageentity import MessageEntity
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 from telegram.constants import InlineQueryResultType
 
 if TYPE_CHECKING:
@@ -47,7 +49,11 @@ class InlineQueryResultAudio(InlineQueryResult):
         audio_url (:obj:`str`): A valid URL for the audio file.
         title (:obj:`str`): Title.
         performer (:obj:`str`, optional): Performer.
-        audio_duration (:obj:`str`, optional): Audio duration in seconds.
+        audio_duration (:obj:`int` | :class:`datetime.timedelta`, optional): Audio duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         caption (:obj:`str`, optional): Caption,
             0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
             parsing.
@@ -69,7 +75,11 @@ class InlineQueryResultAudio(InlineQueryResult):
         audio_url (:obj:`str`): A valid URL for the audio file.
         title (:obj:`str`): Title.
         performer (:obj:`str`): Optional. Performer.
-        audio_duration (:obj:`str`): Optional. Audio duration in seconds.
+        audio_duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Audio duration
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         caption (:obj:`str`): Optional. Caption,
             0-:tg-const:`telegram.constants.MessageLimit.CAPTION_LENGTH` characters after entities
             parsing.
@@ -88,7 +98,7 @@ class InlineQueryResultAudio(InlineQueryResult):
     """
 
     __slots__ = (
-        "audio_duration",
+        "_audio_duration",
         "audio_url",
         "caption",
         "caption_entities",
@@ -105,7 +115,7 @@ def __init__(
         audio_url: str,
         title: str,
         performer: Optional[str] = None,
-        audio_duration: Optional[int] = None,
+        audio_duration: Optional[TimePeriod] = None,
         caption: Optional[str] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
@@ -122,9 +132,13 @@ def __init__(
 
             # Optionals
             self.performer: Optional[str] = performer
-            self.audio_duration: Optional[int] = audio_duration
+            self._audio_duration: Optional[dtm.timedelta] = parse_period_arg(audio_duration)
             self.caption: Optional[str] = caption
             self.parse_mode: ODVInput[str] = parse_mode
             self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
             self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
             self.input_message_content: Optional[InputMessageContent] = input_message_content
+
+    @property
+    def audio_duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._audio_duration, attribute="audio_duration")
diff --git a/src/telegram/_inline/inlinequeryresultgif.py b/src/telegram/_inline/inlinequeryresultgif.py
index 398d61cc79a..fa2c59b03be 100644
--- a/src/telegram/_inline/inlinequeryresultgif.py
+++ b/src/telegram/_inline/inlinequeryresultgif.py
@@ -17,15 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultGif."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
 from telegram._messageentity import MessageEntity
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 from telegram.constants import InlineQueryResultType
 
 if TYPE_CHECKING:
@@ -50,7 +52,11 @@ class InlineQueryResultGif(InlineQueryResult):
         gif_url (:obj:`str`): A valid URL for the GIF file.
         gif_width (:obj:`int`, optional): Width of the GIF.
         gif_height (:obj:`int`, optional): Height of the GIF.
-        gif_duration (:obj:`int`, optional): Duration of the GIF in seconds.
+        gif_duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the GIF
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4)
             thumbnail for the result.
 
@@ -89,7 +95,11 @@ class InlineQueryResultGif(InlineQueryResult):
         gif_url (:obj:`str`): A valid URL for the GIF file.
         gif_width (:obj:`int`): Optional. Width of the GIF.
         gif_height (:obj:`int`): Optional. Height of the GIF.
-        gif_duration (:obj:`int`): Optional. Duration of the GIF in seconds.
+        gif_duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Duration of the GIF
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail
             for the result.
 
@@ -120,9 +130,9 @@ class InlineQueryResultGif(InlineQueryResult):
     """
 
     __slots__ = (
+        "_gif_duration",
         "caption",
         "caption_entities",
-        "gif_duration",
         "gif_height",
         "gif_url",
         "gif_width",
@@ -146,7 +156,7 @@ def __init__(
         caption: Optional[str] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
-        gif_duration: Optional[int] = None,
+        gif_duration: Optional[TimePeriod] = None,
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence[MessageEntity]] = None,
         thumbnail_mime_type: Optional[str] = None,
@@ -163,7 +173,7 @@ def __init__(
             # Optionals
             self.gif_width: Optional[int] = gif_width
             self.gif_height: Optional[int] = gif_height
-            self.gif_duration: Optional[int] = gif_duration
+            self._gif_duration: Optional[dtm.timedelta] = parse_period_arg(gif_duration)
             self.title: Optional[str] = title
             self.caption: Optional[str] = caption
             self.parse_mode: ODVInput[str] = parse_mode
@@ -172,3 +182,7 @@ def __init__(
             self.input_message_content: Optional[InputMessageContent] = input_message_content
             self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type
             self.show_caption_above_media: Optional[bool] = show_caption_above_media
+
+    @property
+    def gif_duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._gif_duration, attribute="gif_duration")
diff --git a/src/telegram/_inline/inlinequeryresultlocation.py b/src/telegram/_inline/inlinequeryresultlocation.py
index 01035537840..b8c41007f18 100644
--- a/src/telegram/_inline/inlinequeryresultlocation.py
+++ b/src/telegram/_inline/inlinequeryresultlocation.py
@@ -18,12 +18,15 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultLocation."""
 
-from typing import TYPE_CHECKING, Final, Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Final, Optional, Union
 
 from telegram import constants
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import InputMessageContent
@@ -48,10 +51,13 @@ class InlineQueryResultLocation(InlineQueryResult):
         horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
             measured in meters; 0-
             :tg-const:`telegram.InlineQueryResultLocation.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`, optional): Period in seconds for which the location will be
-            updated, should be between
+        live_period (:obj:`int` | :class:`datetime.timedelta`, optional): Period in seconds for
+            which the location will be updated, should be between
             :tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and
             :tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD`.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         heading (:obj:`int`, optional): For live locations, a direction in which the user is
             moving, in degrees. Must be between
             :tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and
@@ -86,12 +92,15 @@ class InlineQueryResultLocation(InlineQueryResult):
         horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
             measured in meters; 0-
             :tg-const:`telegram.InlineQueryResultLocation.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`): Optional. Period in seconds for which the location will be
-            updated, should be between
+        live_period (:obj:`int` | :class:`datetime.timedelta`): Optional. Period in seconds for
+            which the location will be updated, should be between
             :tg-const:`telegram.InlineQueryResultLocation.MIN_LIVE_PERIOD` and
             :tg-const:`telegram.InlineQueryResultLocation.MAX_LIVE_PERIOD` or
             :tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
             locations that can be edited indefinitely.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         heading (:obj:`int`): Optional. For live locations, a direction in which the user is
             moving, in degrees. Must be between
             :tg-const:`telegram.InlineQueryResultLocation.MIN_HEADING` and
@@ -118,11 +127,11 @@ class InlineQueryResultLocation(InlineQueryResult):
     """
 
     __slots__ = (
+        "_live_period",
         "heading",
         "horizontal_accuracy",
         "input_message_content",
         "latitude",
-        "live_period",
         "longitude",
         "proximity_alert_radius",
         "reply_markup",
@@ -138,7 +147,7 @@ def __init__(
         latitude: float,
         longitude: float,
         title: str,
-        live_period: Optional[int] = None,
+        live_period: Optional[TimePeriod] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
         horizontal_accuracy: Optional[float] = None,
@@ -158,7 +167,7 @@ def __init__(
             self.title: str = title
 
             # Optionals
-            self.live_period: Optional[int] = live_period
+            self._live_period: Optional[dtm.timedelta] = parse_period_arg(live_period)
             self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
             self.input_message_content: Optional[InputMessageContent] = input_message_content
             self.thumbnail_url: Optional[str] = thumbnail_url
@@ -170,6 +179,10 @@ def __init__(
                 int(proximity_alert_radius) if proximity_alert_radius else None
             )
 
+    @property
+    def live_period(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._live_period, attribute="live_period")
+
     HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY
     """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`
 
diff --git a/src/telegram/_inline/inlinequeryresultmpeg4gif.py b/src/telegram/_inline/inlinequeryresultmpeg4gif.py
index b47faa0186a..fb885d0409c 100644
--- a/src/telegram/_inline/inlinequeryresultmpeg4gif.py
+++ b/src/telegram/_inline/inlinequeryresultmpeg4gif.py
@@ -17,15 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultMpeg4Gif."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
 from telegram._messageentity import MessageEntity
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 from telegram.constants import InlineQueryResultType
 
 if TYPE_CHECKING:
@@ -51,7 +53,11 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
         mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
         mpeg4_width (:obj:`int`, optional): Video width.
         mpeg4_height (:obj:`int`, optional): Video height.
-        mpeg4_duration (:obj:`int`, optional): Video duration in seconds.
+        mpeg4_duration (:obj:`int` | :class:`datetime.timedelta`, optional): Video duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4)
             thumbnail for the result.
 
@@ -91,7 +97,11 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
         mpeg4_url (:obj:`str`): A valid URL for the MP4 file.
         mpeg4_width (:obj:`int`): Optional. Video width.
         mpeg4_height (:obj:`int`): Optional. Video height.
-        mpeg4_duration (:obj:`int`): Optional. Video duration in seconds.
+        mpeg4_duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Video duration
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         thumbnail_url (:obj:`str`): URL of the static (JPEG or GIF) or animated (MPEG4) thumbnail
             for the result.
 
@@ -122,10 +132,10 @@ class InlineQueryResultMpeg4Gif(InlineQueryResult):
     """
 
     __slots__ = (
+        "_mpeg4_duration",
         "caption",
         "caption_entities",
         "input_message_content",
-        "mpeg4_duration",
         "mpeg4_height",
         "mpeg4_url",
         "mpeg4_width",
@@ -148,7 +158,7 @@ def __init__(
         caption: Optional[str] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
-        mpeg4_duration: Optional[int] = None,
+        mpeg4_duration: Optional[TimePeriod] = None,
         parse_mode: ODVInput[str] = DEFAULT_NONE,
         caption_entities: Optional[Sequence[MessageEntity]] = None,
         thumbnail_mime_type: Optional[str] = None,
@@ -165,7 +175,7 @@ def __init__(
             # Optional
             self.mpeg4_width: Optional[int] = mpeg4_width
             self.mpeg4_height: Optional[int] = mpeg4_height
-            self.mpeg4_duration: Optional[int] = mpeg4_duration
+            self._mpeg4_duration: Optional[dtm.timedelta] = parse_period_arg(mpeg4_duration)
             self.title: Optional[str] = title
             self.caption: Optional[str] = caption
             self.parse_mode: ODVInput[str] = parse_mode
@@ -174,3 +184,7 @@ def __init__(
             self.input_message_content: Optional[InputMessageContent] = input_message_content
             self.thumbnail_mime_type: Optional[str] = thumbnail_mime_type
             self.show_caption_above_media: Optional[bool] = show_caption_above_media
+
+    @property
+    def mpeg4_duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._mpeg4_duration, attribute="mpeg4_duration")
diff --git a/src/telegram/_inline/inlinequeryresultvideo.py b/src/telegram/_inline/inlinequeryresultvideo.py
index edc6ce343ac..831ba6e4748 100644
--- a/src/telegram/_inline/inlinequeryresultvideo.py
+++ b/src/telegram/_inline/inlinequeryresultvideo.py
@@ -17,15 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultVideo."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
 from telegram._messageentity import MessageEntity
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 from telegram.constants import InlineQueryResultType
 
 if TYPE_CHECKING:
@@ -73,7 +75,11 @@ class InlineQueryResultVideo(InlineQueryResult):
 
         video_width (:obj:`int`, optional): Video width.
         video_height (:obj:`int`, optional): Video height.
-        video_duration (:obj:`int`, optional): Video duration in seconds.
+        video_duration (:obj:`int` | :class:`datetime.timedelta`, optional): Video duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         description (:obj:`str`, optional): Short description of the result.
         reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
             to the message.
@@ -110,7 +116,11 @@ class InlineQueryResultVideo(InlineQueryResult):
 
         video_width (:obj:`int`): Optional. Video width.
         video_height (:obj:`int`): Optional. Video height.
-        video_duration (:obj:`int`): Optional. Video duration in seconds.
+        video_duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Video duration
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         description (:obj:`str`): Optional. Short description of the result.
         reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
             to the message.
@@ -125,6 +135,7 @@ class InlineQueryResultVideo(InlineQueryResult):
     """
 
     __slots__ = (
+        "_video_duration",
         "caption",
         "caption_entities",
         "description",
@@ -135,7 +146,6 @@ class InlineQueryResultVideo(InlineQueryResult):
         "show_caption_above_media",
         "thumbnail_url",
         "title",
-        "video_duration",
         "video_height",
         "video_url",
         "video_width",
@@ -151,7 +161,7 @@ def __init__(
         caption: Optional[str] = None,
         video_width: Optional[int] = None,
         video_height: Optional[int] = None,
-        video_duration: Optional[int] = None,
+        video_duration: Optional[TimePeriod] = None,
         description: Optional[str] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
@@ -175,8 +185,12 @@ def __init__(
             self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
             self.video_width: Optional[int] = video_width
             self.video_height: Optional[int] = video_height
-            self.video_duration: Optional[int] = video_duration
+            self._video_duration: Optional[dtm.timedelta] = parse_period_arg(video_duration)
             self.description: Optional[str] = description
             self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
             self.input_message_content: Optional[InputMessageContent] = input_message_content
             self.show_caption_above_media: Optional[bool] = show_caption_above_media
+
+    @property
+    def video_duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._video_duration, attribute="video_duration")
diff --git a/src/telegram/_inline/inlinequeryresultvoice.py b/src/telegram/_inline/inlinequeryresultvoice.py
index b798040b1aa..5b5b0f42ec8 100644
--- a/src/telegram/_inline/inlinequeryresultvoice.py
+++ b/src/telegram/_inline/inlinequeryresultvoice.py
@@ -17,15 +17,17 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InlineQueryResultVoice."""
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._inline.inlinekeyboardmarkup import InlineKeyboardMarkup
 from telegram._inline.inlinequeryresult import InlineQueryResult
 from telegram._messageentity import MessageEntity
-from telegram._utils.argumentparsing import parse_sequence_arg
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import get_timedelta_value
 from telegram._utils.defaultvalue import DEFAULT_NONE
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 from telegram.constants import InlineQueryResultType
 
 if TYPE_CHECKING:
@@ -56,7 +58,11 @@ class InlineQueryResultVoice(InlineQueryResult):
             .. versionchanged:: 20.0
                 |sequenceclassargs|
 
-        voice_duration (:obj:`int`, optional): Recording duration in seconds.
+        voice_duration (:obj:`int` | :class:`datetime.timedelta`, optional): Recording duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached
             to the message.
         input_message_content (:class:`telegram.InputMessageContent`, optional): Content of the
@@ -79,7 +85,11 @@ class InlineQueryResultVoice(InlineQueryResult):
 
                 * |tupleclassattrs|
                 * |alwaystuple|
-        voice_duration (:obj:`int`): Optional. Recording duration in seconds.
+        voice_duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Recording duration
+            in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached
             to the message.
         input_message_content (:class:`telegram.InputMessageContent`): Optional. Content of the
@@ -88,13 +98,13 @@ class InlineQueryResultVoice(InlineQueryResult):
     """
 
     __slots__ = (
+        "_voice_duration",
         "caption",
         "caption_entities",
         "input_message_content",
         "parse_mode",
         "reply_markup",
         "title",
-        "voice_duration",
         "voice_url",
     )
 
@@ -103,7 +113,7 @@ def __init__(
         id: str,  # pylint: disable=redefined-builtin
         voice_url: str,
         title: str,
-        voice_duration: Optional[int] = None,
+        voice_duration: Optional[TimePeriod] = None,
         caption: Optional[str] = None,
         reply_markup: Optional[InlineKeyboardMarkup] = None,
         input_message_content: Optional["InputMessageContent"] = None,
@@ -119,9 +129,13 @@ def __init__(
             self.title: str = title
 
             # Optional
-            self.voice_duration: Optional[int] = voice_duration
+            self._voice_duration: Optional[dtm.timedelta] = parse_period_arg(voice_duration)
             self.caption: Optional[str] = caption
             self.parse_mode: ODVInput[str] = parse_mode
             self.caption_entities: tuple[MessageEntity, ...] = parse_sequence_arg(caption_entities)
             self.reply_markup: Optional[InlineKeyboardMarkup] = reply_markup
             self.input_message_content: Optional[InputMessageContent] = input_message_content
+
+    @property
+    def voice_duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._voice_duration, attribute="voice_duration")
diff --git a/src/telegram/_inline/inputlocationmessagecontent.py b/src/telegram/_inline/inputlocationmessagecontent.py
index f71a716c259..2c7228977c4 100644
--- a/src/telegram/_inline/inputlocationmessagecontent.py
+++ b/src/telegram/_inline/inputlocationmessagecontent.py
@@ -18,11 +18,14 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains the classes that represent Telegram InputLocationMessageContent."""
 
-from typing import Final, Optional
+import datetime as dtm
+from typing import Final, Optional, Union
 
 from telegram import constants
 from telegram._inline.inputmessagecontent import InputMessageContent
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
 
 
 class InputLocationMessageContent(InputMessageContent):
@@ -39,12 +42,15 @@ class InputLocationMessageContent(InputMessageContent):
         horizontal_accuracy (:obj:`float`, optional): The radius of uncertainty for the location,
             measured in meters; 0-
             :tg-const:`telegram.InputLocationMessageContent.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`, optional): Period in seconds for which the location will be
-            updated, should be between
+        live_period (:obj:`int` | :class:`datetime.timedelta`, optional): Period in seconds for
+            which the location will be updated, should be between
             :tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and
             :tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD` or
             :tg-const:`telegram.constants.LocationLimit.LIVE_PERIOD_FOREVER` for live
             locations that can be edited indefinitely.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         heading (:obj:`int`, optional): For live locations, a direction in which the user is
             moving, in degrees. Must be between
             :tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and
@@ -61,10 +67,13 @@ class InputLocationMessageContent(InputMessageContent):
         horizontal_accuracy (:obj:`float`): Optional. The radius of uncertainty for the location,
             measured in meters; 0-
             :tg-const:`telegram.InputLocationMessageContent.HORIZONTAL_ACCURACY`.
-        live_period (:obj:`int`): Optional. Period in seconds for which the location can be
-            updated, should be between
+        live_period (:obj:`int` | :class:`datetime.timedelta`): Optional. Period in seconds for
+            which the location can be updated, should be between
             :tg-const:`telegram.InputLocationMessageContent.MIN_LIVE_PERIOD` and
             :tg-const:`telegram.InputLocationMessageContent.MAX_LIVE_PERIOD`.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         heading (:obj:`int`): Optional. For live locations, a direction in which the user is
             moving, in degrees. Must be between
             :tg-const:`telegram.InputLocationMessageContent.MIN_HEADING` and
@@ -78,19 +87,20 @@ class InputLocationMessageContent(InputMessageContent):
     """
 
     __slots__ = (
+        "_live_period",
         "heading",
         "horizontal_accuracy",
         "latitude",
-        "live_period",
         "longitude",
-        "proximity_alert_radius")
+        "proximity_alert_radius",
+    )
     # fmt: on
 
     def __init__(
         self,
         latitude: float,
         longitude: float,
-        live_period: Optional[int] = None,
+        live_period: Optional[TimePeriod] = None,
         horizontal_accuracy: Optional[float] = None,
         heading: Optional[int] = None,
         proximity_alert_radius: Optional[int] = None,
@@ -104,7 +114,7 @@ def __init__(
             self.longitude: float = longitude
 
             # Optionals
-            self.live_period: Optional[int] = live_period
+            self._live_period: Optional[dtm.timedelta] = parse_period_arg(live_period)
             self.horizontal_accuracy: Optional[float] = horizontal_accuracy
             self.heading: Optional[int] = heading
             self.proximity_alert_radius: Optional[int] = (
@@ -113,6 +123,10 @@ def __init__(
 
             self._id_attrs = (self.latitude, self.longitude)
 
+    @property
+    def live_period(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._live_period, attribute="live_period")
+
     HORIZONTAL_ACCURACY: Final[int] = constants.LocationLimit.HORIZONTAL_ACCURACY
     """:const:`telegram.constants.LocationLimit.HORIZONTAL_ACCURACY`
 
diff --git a/src/telegram/_messageautodeletetimerchanged.py b/src/telegram/_messageautodeletetimerchanged.py
index 1653c050d59..0fc0eb78c6a 100644
--- a/src/telegram/_messageautodeletetimerchanged.py
+++ b/src/telegram/_messageautodeletetimerchanged.py
@@ -20,10 +20,16 @@
 deletion.
 """
 
-from typing import Optional
+import datetime as dtm
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._telegramobject import TelegramObject
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
+
+if TYPE_CHECKING:
+    from telegram import Bot
 
 
 class MessageAutoDeleteTimerChanged(TelegramObject):
@@ -35,26 +41,52 @@ class MessageAutoDeleteTimerChanged(TelegramObject):
     .. versionadded:: 13.4
 
     Args:
-        message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the
-            chat.
+        message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`): New auto-delete time
+            for messages in the chat.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
-        message_auto_delete_time (:obj:`int`): New auto-delete time for messages in the
-            chat.
+        message_auto_delete_time (:obj:`int` | :class:`datetime.timedelta`): New auto-delete time
+            for messages in the chat.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
 
     """
 
-    __slots__ = ("message_auto_delete_time",)
+    __slots__ = ("_message_auto_delete_time",)
 
     def __init__(
         self,
-        message_auto_delete_time: int,
+        message_auto_delete_time: TimePeriod,
         *,
         api_kwargs: Optional[JSONDict] = None,
     ):
         super().__init__(api_kwargs=api_kwargs)
-        self.message_auto_delete_time: int = message_auto_delete_time
+        self._message_auto_delete_time: dtm.timedelta = parse_period_arg(
+            message_auto_delete_time
+        )  # type: ignore[assignment]
 
         self._id_attrs = (self.message_auto_delete_time,)
 
         self._freeze()
+
+    @property
+    def message_auto_delete_time(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._message_auto_delete_time, attribute="message_auto_delete_time"
+        )
+
+    @classmethod
+    def de_json(
+        cls, data: JSONDict, bot: Optional["Bot"] = None
+    ) -> "MessageAutoDeleteTimerChanged":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+        data["message_auto_delete_time"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("message_auto_delete_time")) else None
+        )
+
+        return super().de_json(data=data, bot=bot)
diff --git a/src/telegram/_paidmedia.py b/src/telegram/_paidmedia.py
index 972c46fa333..eac8b730373 100644
--- a/src/telegram/_paidmedia.py
+++ b/src/telegram/_paidmedia.py
@@ -18,8 +18,9 @@
 # along with this program. If not, see [http://www.gnu.org/licenses/].
 """This module contains objects that represent paid media in Telegram."""
 
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Final, Optional
+from typing import TYPE_CHECKING, Final, Optional, Union
 
 from telegram import constants
 from telegram._files.photosize import PhotoSize
@@ -27,8 +28,14 @@
 from telegram._telegramobject import TelegramObject
 from telegram._user import User
 from telegram._utils import enum
-from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import (
+    de_json_optional,
+    de_list_optional,
+    parse_period_arg,
+    parse_sequence_arg,
+)
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import JSONDict, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import Bot
@@ -98,6 +105,9 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "PaidMedia":
         if cls is PaidMedia and data.get("type") in _class_mapping:
             return _class_mapping[data.pop("type")].de_json(data=data, bot=bot)
 
+        if "duration" in data:
+            data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
         return super().de_json(data=data, bot=bot)
 
 
@@ -110,26 +120,38 @@ class PaidMediaPreview(PaidMedia):
 
     .. versionadded:: 21.4
 
+    .. versionchanged:: NEXT.VERSION
+       As part of the migration to representing time periods using ``datetime.timedelta``,
+       equality comparison now considers integer durations and equivalent timedeltas as equal.
+
     Args:
         type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`.
         width (:obj:`int`, optional): Media width as defined by the sender.
         height (:obj:`int`, optional): Media height as defined by the sender.
-        duration (:obj:`int`, optional): Duration of the media in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`, optional): Duration of the media in
+            seconds as defined by the sender.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
         type (:obj:`str`): Type of the paid media, always :tg-const:`telegram.PaidMedia.PREVIEW`.
         width (:obj:`int`): Optional. Media width as defined by the sender.
         height (:obj:`int`): Optional. Media height as defined by the sender.
-        duration (:obj:`int`): Optional. Duration of the media in seconds as defined by the sender.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Optional. Duration of the media in
+            seconds as defined by the sender.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
     """
 
-    __slots__ = ("duration", "height", "width")
+    __slots__ = ("_duration", "height", "width")
 
     def __init__(
         self,
         width: Optional[int] = None,
         height: Optional[int] = None,
-        duration: Optional[int] = None,
+        duration: Optional[TimePeriod] = None,
         *,
         api_kwargs: Optional[JSONDict] = None,
     ) -> None:
@@ -138,9 +160,13 @@ def __init__(
         with self._unfrozen():
             self.width: Optional[int] = width
             self.height: Optional[int] = height
-            self.duration: Optional[int] = duration
+            self._duration: Optional[dtm.timedelta] = parse_period_arg(duration)
+
+            self._id_attrs = (self.type, self.width, self.height, self._duration)
 
-            self._id_attrs = (self.type, self.width, self.height, self.duration)
+    @property
+    def duration(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._duration, attribute="duration")
 
 
 class PaidMediaPhoto(PaidMedia):
diff --git a/src/telegram/_payment/stars/staramount.py b/src/telegram/_payment/stars/staramount.py
index a8d61b2a118..c78a4aa9aba 100644
--- a/src/telegram/_payment/stars/staramount.py
+++ b/src/telegram/_payment/stars/staramount.py
@@ -16,7 +16,6 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program. If not, see [http://www.gnu.org/licenses/].
-# pylint: disable=redefined-builtin
 """This module contains an object that represents a Telegram StarAmount."""
 
 
diff --git a/src/telegram/_poll.py b/src/telegram/_poll.py
index 8ecdc4105f9..4784febd16c 100644
--- a/src/telegram/_poll.py
+++ b/src/telegram/_poll.py
@@ -19,7 +19,7 @@
 """This module contains an object that represents a Telegram Poll."""
 import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Final, Optional
+from typing import TYPE_CHECKING, Final, Optional, Union
 
 from telegram import constants
 from telegram._chat import Chat
@@ -27,11 +27,20 @@
 from telegram._telegramobject import TelegramObject
 from telegram._user import User
 from telegram._utils import enum
-from telegram._utils.argumentparsing import de_json_optional, de_list_optional, parse_sequence_arg
-from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
+from telegram._utils.argumentparsing import (
+    de_json_optional,
+    de_list_optional,
+    parse_period_arg,
+    parse_sequence_arg,
+)
+from telegram._utils.datetime import (
+    extract_tzinfo_from_defaults,
+    from_timestamp,
+    get_timedelta_value,
+)
 from telegram._utils.defaultvalue import DEFAULT_NONE
 from telegram._utils.entities import parse_message_entities, parse_message_entity
-from telegram._utils.types import JSONDict, ODVInput
+from telegram._utils.types import JSONDict, ODVInput, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import Bot
@@ -343,8 +352,11 @@ class Poll(TelegramObject):
 
                * This attribute is now always a (possibly empty) list and never :obj:`None`.
                * |sequenceclassargs|
-        open_period (:obj:`int`, optional): Amount of time in seconds the poll will be active
-            after creation.
+        open_period (:obj:`int` | :class:`datetime.timedelta`, optional): Amount of time in seconds
+            the poll will be active after creation.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
         close_date (:obj:`datetime.datetime`, optional): Point in time (Unix timestamp) when the
             poll will be automatically closed. Converted to :obj:`datetime.datetime`.
 
@@ -384,8 +396,11 @@ class Poll(TelegramObject):
 
             .. versionchanged:: 20.0
                This attribute is now always a (possibly empty) list and never :obj:`None`.
-        open_period (:obj:`int`): Optional. Amount of time in seconds the poll will be active
-            after creation.
+        open_period (:obj:`int` | :class:`datetime.timedelta`): Optional. Amount of time in seconds
+            the poll will be active after creation.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
         close_date (:obj:`datetime.datetime`): Optional. Point in time when the poll will be
             automatically closed.
 
@@ -401,6 +416,7 @@ class Poll(TelegramObject):
     """
 
     __slots__ = (
+        "_open_period",
         "allows_multiple_answers",
         "close_date",
         "correct_option_id",
@@ -409,7 +425,6 @@ class Poll(TelegramObject):
         "id",
         "is_anonymous",
         "is_closed",
-        "open_period",
         "options",
         "question",
         "question_entities",
@@ -430,7 +445,7 @@ def __init__(
         correct_option_id: Optional[int] = None,
         explanation: Optional[str] = None,
         explanation_entities: Optional[Sequence[MessageEntity]] = None,
-        open_period: Optional[int] = None,
+        open_period: Optional[TimePeriod] = None,
         close_date: Optional[dtm.datetime] = None,
         question_entities: Optional[Sequence[MessageEntity]] = None,
         *,
@@ -450,7 +465,7 @@ def __init__(
         self.explanation_entities: tuple[MessageEntity, ...] = parse_sequence_arg(
             explanation_entities
         )
-        self.open_period: Optional[int] = open_period
+        self._open_period: Optional[dtm.timedelta] = parse_period_arg(open_period)
         self.close_date: Optional[dtm.datetime] = close_date
         self.question_entities: tuple[MessageEntity, ...] = parse_sequence_arg(question_entities)
 
@@ -458,6 +473,10 @@ def __init__(
 
         self._freeze()
 
+    @property
+    def open_period(self) -> Optional[Union[int, dtm.timedelta]]:
+        return get_timedelta_value(self._open_period, attribute="open_period")
+
     @classmethod
     def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Poll":
         """See :meth:`telegram.TelegramObject.de_json`."""
@@ -474,6 +493,7 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "Poll":
         data["question_entities"] = de_list_optional(
             data.get("question_entities"), MessageEntity, bot
         )
+        data["open_period"] = dtm.timedelta(seconds=s) if (s := data.get("open_period")) else None
 
         return super().de_json(data=data, bot=bot)
 
diff --git a/src/telegram/_telegramobject.py b/src/telegram/_telegramobject.py
index ca0d20555eb..aabedeb9f05 100644
--- a/src/telegram/_telegramobject.py
+++ b/src/telegram/_telegramobject.py
@@ -499,6 +499,13 @@ def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None:
             elif getattr(self, key, True) is None:
                 setattr(self, key, api_kwargs.pop(key))
 
+    def _is_deprecated_attr(self, attr: str) -> bool:
+        """Checks wheather `attr` is in the list of deprecated time period attributes."""
+        return (
+            (class_name := self.__class__.__name__) in TelegramObject._TIME_PERIOD_DEPRECATIONS
+            and attr in TelegramObject._TIME_PERIOD_DEPRECATIONS[class_name]
+        )
+
     def _get_attrs_names(self, include_private: bool) -> Iterator[str]:
         """
         Returns the names of the attributes of this object. This is used to determine which
@@ -521,7 +528,11 @@ def _get_attrs_names(self, include_private: bool) -> Iterator[str]:
 
         if include_private:
             return all_attrs
-        return (attr for attr in all_attrs if not attr.startswith("_"))
+        return (
+            attr
+            for attr in all_attrs
+            if not attr.startswith("_") or self._is_deprecated_attr(attr)
+        )
 
     def _get_attrs(
         self,
@@ -558,7 +569,7 @@ def _get_attrs(
                 if recursive and hasattr(value, "to_dict"):
                     data[key] = value.to_dict(recursive=True)
                 else:
-                    data[key] = value
+                    data[key.removeprefix("_") if self._is_deprecated_attr(key) else key] = value
             elif not recursive:
                 data[key] = value
 
@@ -629,7 +640,11 @@ def to_dict(self, recursive: bool = True) -> JSONDict:
             elif isinstance(value, dtm.datetime):
                 out[key] = to_timestamp(value)
             elif isinstance(value, dtm.timedelta):
-                out[key] = value.total_seconds()
+                # Converting to int here is neccassry in some cases where Bot API returns
+                # 'BadRquest' when expecting integers (e.g. Video.duration)
+                out[key] = (
+                    int(seconds) if (seconds := value.total_seconds()).is_integer() else seconds
+                )
 
         for key in pop_keys:
             out.pop(key)
@@ -665,3 +680,30 @@ def set_bot(self, bot: Optional["Bot"]) -> None:
             bot (:class:`telegram.Bot` | :obj:`None`): The bot instance.
         """
         self._bot = bot
+
+    # We use str keys to avoid importing which causes circular dependencies
+    _TIME_PERIOD_DEPRECATIONS: ClassVar = {
+        "ChatFullInfo": ("_message_auto_delete_time", "_slow_mode_delay"),
+        "Animation": ("_duration",),
+        "Audio": ("_duration",),
+        "Video": ("_duration", "_start_timestamp"),
+        "VideoNote": ("_duration",),
+        "Voice": ("_duration",),
+        "PaidMediaPreview": ("_duration",),
+        "VideoChatEnded": ("_duration",),
+        "InputMediaVideo": ("_duration",),
+        "InputMediaAnimation": ("_duration",),
+        "InputMediaAudio": ("_duration",),
+        "InputPaidMediaVideo": ("_duration",),
+        "InlineQueryResultGif": ("_gif_duration",),
+        "InlineQueryResultMpeg4Gif": ("_mpeg4_duration",),
+        "InlineQueryResultVideo": ("_video_duration",),
+        "InlineQueryResultAudio": ("_audio_duration",),
+        "InlineQueryResultVoice": ("_voice_duration",),
+        "InlineQueryResultLocation": ("_live_period",),
+        "Poll": ("_open_period",),
+        "Location": ("_live_period",),
+        "MessageAutoDeleteTimerChanged": ("_message_auto_delete_time",),
+        "ChatInviteLink": ("_subscription_period",),
+        "InputLocationMessageContent": ("_live_period",),
+    }
diff --git a/src/telegram/_utils/argumentparsing.py b/src/telegram/_utils/argumentparsing.py
index 84ca1bc6a2f..70ef767cbed 100644
--- a/src/telegram/_utils/argumentparsing.py
+++ b/src/telegram/_utils/argumentparsing.py
@@ -23,8 +23,9 @@
     user. Changes to this module are not considered breaking changes and may not be documented in
     the changelog.
 """
+import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional, Protocol, TypeVar
+from typing import TYPE_CHECKING, Optional, Protocol, TypeVar, Union
 
 from telegram._linkpreviewoptions import LinkPreviewOptions
 from telegram._telegramobject import TelegramObject
@@ -50,6 +51,22 @@ def parse_sequence_arg(arg: Optional[Sequence[T]]) -> tuple[T, ...]:
     return tuple(arg) if arg else ()
 
 
+def parse_period_arg(arg: Optional[Union[int, float, dtm.timedelta]]) -> Optional[dtm.timedelta]:
+    """Parses an optional time period in seconds into a timedelta
+
+    Args:
+        arg (:obj:`int` | :class:`datetime.timedelta`, optional): The time period to parse.
+
+    Returns:
+        :obj:`timedelta`: The time period converted to a timedelta object or :obj:`None`.
+    """
+    if arg is None:
+        return None
+    if isinstance(arg, (int, float)):
+        return dtm.timedelta(seconds=arg)
+    return arg
+
+
 def parse_lpo_and_dwpp(
     disable_web_page_preview: Optional[bool], link_preview_options: ODVInput[LinkPreviewOptions]
 ) -> ODVInput[LinkPreviewOptions]:
diff --git a/src/telegram/_utils/datetime.py b/src/telegram/_utils/datetime.py
index 8e6ebdda1b4..f3bf2abec43 100644
--- a/src/telegram/_utils/datetime.py
+++ b/src/telegram/_utils/datetime.py
@@ -29,9 +29,13 @@
 """
 import contextlib
 import datetime as dtm
+import os
 import time
 from typing import TYPE_CHECKING, Optional, Union
 
+from telegram._utils.warnings import warn
+from telegram.warnings import PTBDeprecationWarning
+
 if TYPE_CHECKING:
     from telegram import Bot
 
@@ -224,3 +228,47 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
     if dt_obj.tzinfo is None:
         dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
     return dt_obj.timestamp()
+
+
+def get_timedelta_value(
+    value: Optional[dtm.timedelta], attribute: str
+) -> Optional[Union[int, dtm.timedelta]]:
+    """
+    Convert a `datetime.timedelta` to seconds or return it as-is, based on environment config.
+
+    This utility is part of the migration process from integer-based time representations
+    to using `datetime.timedelta`. The behavior is controlled by the `PTB_TIMEDELTA`
+    environment variable.
+
+    Note:
+        When `PTB_TIMEDELTA` is not enabled, the function will issue a deprecation warning.
+
+    Args:
+        value (:obj:`datetime.timedelta`): The timedelta value to process.
+        attribute (:obj:`str`): The name of the attribute at the caller scope, used for
+            warning messages.
+
+    Returns:
+        - :obj:`None` if :paramref:`value` is None.
+        - :obj:`datetime.timedelta` if `PTB_TIMEDELTA=true`.
+        - :obj:`int` if the total seconds is a whole number.
+        - float: otherwise.
+    """
+    if value is None:
+        return None
+    if os.getenv("PTB_TIMEDELTA", "false").lower().strip() == "true":
+        return value
+    warn(
+        PTBDeprecationWarning(
+            "NEXT.VERSION",
+            f"In a future major version attribute `{attribute}` will be of type"
+            " `datetime.timedelta`. You can opt-in early by setting `PTB_TIMEDELTA=true`"
+            " as an environment variable.",
+        ),
+        stacklevel=2,
+    )
+    return (
+        int(seconds)
+        if (seconds := value.total_seconds()).is_integer()
+        else seconds  # type: ignore[return-value]
+    )
diff --git a/src/telegram/_videochat.py b/src/telegram/_videochat.py
index 7c1ec00aabb..bf5b4ee3593 100644
--- a/src/telegram/_videochat.py
+++ b/src/telegram/_videochat.py
@@ -19,13 +19,17 @@
 """This module contains objects related to Telegram video chats."""
 import datetime as dtm
 from collections.abc import Sequence
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING, Optional, Union
 
 from telegram._telegramobject import TelegramObject
 from telegram._user import User
-from telegram._utils.argumentparsing import parse_sequence_arg
-from telegram._utils.datetime import extract_tzinfo_from_defaults, from_timestamp
-from telegram._utils.types import JSONDict
+from telegram._utils.argumentparsing import parse_period_arg, parse_sequence_arg
+from telegram._utils.datetime import (
+    extract_tzinfo_from_defaults,
+    from_timestamp,
+    get_timedelta_value,
+)
+from telegram._utils.types import JSONDict, TimePeriod
 
 if TYPE_CHECKING:
     from telegram import Bot
@@ -62,28 +66,54 @@ class VideoChatEnded(TelegramObject):
     .. versionchanged:: 20.0
         This class was renamed from ``VoiceChatEnded`` in accordance to Bot API 6.0.
 
+    .. versionchanged:: NEXT.VERSION
+       As part of the migration to representing time periods using ``datetime.timedelta``,
+       equality comparison now considers integer durations and equivalent timedeltas as equal.
+
     Args:
-        duration (:obj:`int`): Voice chat duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Voice chat duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
-        duration (:obj:`int`): Voice chat duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Voice chat duration in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
 
     """
 
-    __slots__ = ("duration",)
+    __slots__ = ("_duration",)
 
     def __init__(
         self,
-        duration: int,
+        duration: TimePeriod,
         *,
         api_kwargs: Optional[JSONDict] = None,
     ) -> None:
         super().__init__(api_kwargs=api_kwargs)
-        self.duration: int = duration
-        self._id_attrs = (self.duration,)
+        self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
+        self._id_attrs = (self._duration,)
 
         self._freeze()
 
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "VideoChatEnded":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
+
 
 class VideoChatParticipantsInvited(TelegramObject):
     """
diff --git a/src/telegram/error.py b/src/telegram/error.py
index 2de0361762d..309836febd5 100644
--- a/src/telegram/error.py
+++ b/src/telegram/error.py
@@ -22,6 +22,13 @@
     Replaced ``Unauthorized`` by :class:`Forbidden`.
 """
 
+import datetime as dtm
+from typing import Optional, Union
+
+from telegram._utils.argumentparsing import parse_period_arg
+from telegram._utils.datetime import get_timedelta_value
+from telegram._utils.types import TimePeriod
+
 __all__ = (
     "BadRequest",
     "ChatMigrated",
@@ -36,8 +43,6 @@
     "TimedOut",
 )
 
-from typing import Optional, Union
-
 
 class TelegramError(Exception):
     """
@@ -208,21 +213,43 @@ class RetryAfter(TelegramError):
        :attr:`retry_after` is now an integer to comply with the Bot API.
 
     Args:
-        retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
+        retry_after (:obj:`int` | :class:`datetime.timedelta`): Time in seconds, after which the
+            bot can retry the request.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
-        retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
+        retry_after (:obj:`int` | :class:`datetime.timedelta`): Time in seconds, after which the
+            bot can retry the request.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
 
     """
 
-    __slots__ = ("retry_after",)
+    __slots__ = ("_retry_after",)
+
+    def __init__(self, retry_after: TimePeriod):
+        self._retry_after: dtm.timedelta = parse_period_arg(  # type: ignore[assignment]
+            retry_after
+        )
+
+        if isinstance(self.retry_after, int):
+            super().__init__(f"Flood control exceeded. Retry in {self.retry_after} seconds")
+        else:
+            super().__init__(f"Flood control exceeded. Retry in {self.retry_after!s}")
 
-    def __init__(self, retry_after: int):
-        super().__init__(f"Flood control exceeded. Retry in {retry_after} seconds")
-        self.retry_after: int = retry_after
+    @property
+    def retry_after(self) -> Union[int, dtm.timedelta]:  # noqa: D102
+        # Diableing D102 because docstring for `retry_after` is present at the class's level
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._retry_after, attribute="retry_after"
+        )
 
     def __reduce__(self) -> tuple[type, tuple[float]]:  # type: ignore[override]
-        return self.__class__, (self.retry_after,)
+        # Until support for `int` time periods is lifted, leave pickle behaviour the same
+        return self.__class__, (int(self._retry_after.total_seconds()),)
 
 
 class Conflict(TelegramError):
diff --git a/src/telegram/ext/_aioratelimiter.py b/src/telegram/ext/_aioratelimiter.py
index f4ecf917f66..d2d537e7e27 100644
--- a/src/telegram/ext/_aioratelimiter.py
+++ b/src/telegram/ext/_aioratelimiter.py
@@ -288,7 +288,7 @@ async def process_request(
                     )
                     raise
 
-                sleep = exc.retry_after + 0.1
+                sleep = exc._retry_after.total_seconds() + 0.1  # pylint: disable=protected-access
                 _LOGGER.info("Rate limit hit. Retrying after %f seconds", sleep)
                 # Make sure we don't allow other requests to be processed
                 self._retry_after_event.clear()
diff --git a/src/telegram/ext/_application.py b/src/telegram/ext/_application.py
index e856fa85321..30a786cf038 100644
--- a/src/telegram/ext/_application.py
+++ b/src/telegram/ext/_application.py
@@ -20,6 +20,7 @@
 
 import asyncio
 import contextlib
+import datetime as dtm
 import inspect
 import itertools
 import platform
@@ -42,7 +43,7 @@
 )
 from telegram._utils.logging import get_logger
 from telegram._utils.repr import build_repr_with_selected_attrs
-from telegram._utils.types import SCT, DVType, ODVInput
+from telegram._utils.types import SCT, DVType, ODVInput, TimePeriod
 from telegram._utils.warnings import warn
 from telegram.error import TelegramError
 from telegram.ext._basepersistence import BasePersistence
@@ -739,7 +740,7 @@ def stop_running(self) -> None:
     def run_polling(
         self,
         poll_interval: float = 0.0,
-        timeout: int = 10,
+        timeout: TimePeriod = dtm.timedelta(seconds=10),
         bootstrap_retries: int = 0,
         allowed_updates: Optional[Sequence[str]] = None,
         drop_pending_updates: Optional[bool] = None,
@@ -780,8 +781,11 @@ def run_polling(
         Args:
             poll_interval (:obj:`float`, optional): Time to wait between polling updates from
                 Telegram in seconds. Default is ``0.0``.
-            timeout (:obj:`int`, optional): Passed to
-                :paramref:`telegram.Bot.get_updates.timeout`. Default is ``10`` seconds.
+            timeout (:obj:`int` | :class:`datetime.timedelta`, optional): Passed to
+                :paramref:`telegram.Bot.get_updates.timeout`. Default is ``timedelta(seconds=10)``.
+
+                .. versionchanged:: NEXT.VERSION
+                    |time-period-input|
             bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase
                 (calling :meth:`initialize` and the boostrapping of
                 :meth:`telegram.ext.Updater.start_polling`)
diff --git a/src/telegram/ext/_extbot.py b/src/telegram/ext/_extbot.py
index 7afadaa89fa..5781cf817bc 100644
--- a/src/telegram/ext/_extbot.py
+++ b/src/telegram/ext/_extbot.py
@@ -657,7 +657,7 @@ async def get_updates(
         self,
         offset: Optional[int] = None,
         limit: Optional[int] = None,
-        timeout: Optional[int] = None,
+        timeout: Optional[TimePeriod] = None,
         allowed_updates: Optional[Sequence[str]] = None,
         *,
         read_timeout: ODVInput[float] = DEFAULT_NONE,
diff --git a/src/telegram/ext/_updater.py b/src/telegram/ext/_updater.py
index 95f7e225ed1..63634fbc467 100644
--- a/src/telegram/ext/_updater.py
+++ b/src/telegram/ext/_updater.py
@@ -20,6 +20,7 @@
 
 import asyncio
 import contextlib
+import datetime as dtm
 import ssl
 from collections.abc import Coroutine, Sequence
 from pathlib import Path
@@ -29,7 +30,7 @@
 from telegram._utils.defaultvalue import DEFAULT_80, DEFAULT_IP, DefaultValue
 from telegram._utils.logging import get_logger
 from telegram._utils.repr import build_repr_with_selected_attrs
-from telegram._utils.types import DVType
+from telegram._utils.types import DVType, TimePeriod
 from telegram.error import TelegramError
 from telegram.ext._utils.networkloop import network_retry_loop
 
@@ -206,7 +207,7 @@ async def shutdown(self) -> None:
     async def start_polling(
         self,
         poll_interval: float = 0.0,
-        timeout: int = 10,
+        timeout: TimePeriod = dtm.timedelta(seconds=10),
         bootstrap_retries: int = 0,
         allowed_updates: Optional[Sequence[str]] = None,
         drop_pending_updates: Optional[bool] = None,
@@ -226,8 +227,12 @@ async def start_polling(
         Args:
             poll_interval (:obj:`float`, optional): Time to wait between polling updates from
                 Telegram in seconds. Default is ``0.0``.
-            timeout (:obj:`int`, optional): Passed to
-                :paramref:`telegram.Bot.get_updates.timeout`. Defaults to ``10`` seconds.
+            timeout (:obj:`int` | :class:`datetime.timedelta`, optional): Passed to
+                :paramref:`telegram.Bot.get_updates.timeout`. Defaults to
+                ``timedelta(seconds=10)``.
+
+                .. versionchanged:: NEXT.VERSION
+                    |time-period-input|
             bootstrap_retries (:obj:`int`, optional): Whether the bootstrapping phase of
                 will retry on failures on the Telegram server.
 
@@ -309,7 +314,7 @@ def callback(error: telegram.error.TelegramError)
     async def _start_polling(
         self,
         poll_interval: float,
-        timeout: int,
+        timeout: TimePeriod,
         bootstrap_retries: int,
         drop_pending_updates: Optional[bool],
         allowed_updates: Optional[Sequence[str]],
@@ -394,7 +399,7 @@ async def _get_updates_cleanup() -> None:
                 await self.bot.get_updates(
                     offset=self._last_update_id,
                     # We don't want to do long polling here!
-                    timeout=0,
+                    timeout=dtm.timedelta(seconds=0),
                     allowed_updates=allowed_updates,
                 )
             except TelegramError:
diff --git a/src/telegram/ext/_utils/networkloop.py b/src/telegram/ext/_utils/networkloop.py
index 03c54e8e8a2..2cc93113272 100644
--- a/src/telegram/ext/_utils/networkloop.py
+++ b/src/telegram/ext/_utils/networkloop.py
@@ -119,7 +119,8 @@ async def do_action() -> bool:
             _LOGGER.info(
                 "%s %s. Adding %s seconds to the specified time.", log_prefix, exc, slack_time
             )
-            cur_interval = slack_time + exc.retry_after
+            # pylint: disable=protected-access
+            cur_interval = slack_time + exc._retry_after.total_seconds()
         except TimedOut as toe:
             _LOGGER.debug("%s Timed out: %s. Retrying immediately.", log_prefix, toe)
             # If failure is due to timeout, we should retry asap.
diff --git a/tests/_files/test_animation.py b/tests/_files/test_animation.py
index 5ae93dd61ef..50437e69877 100644
--- a/tests/_files/test_animation.py
+++ b/tests/_files/test_animation.py
@@ -28,6 +28,7 @@
 from telegram.error import BadRequest, TelegramError
 from telegram.helpers import escape_markdown
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.bot_method_checks import (
     check_defaults_handling,
     check_shortcut_call,
@@ -43,7 +44,7 @@ class AnimationTestBase:
     animation_file_unique_id = "adc3145fd2e84d95b64d68eaa22aa33e"
     width = 320
     height = 180
-    duration = 1
+    duration = dtm.timedelta(seconds=1)
     # animation_file_url = 'https://python-telegram-bot.org/static/testfiles/game.gif'
     # Shortened link, the above one is cached with the wrong duration.
     animation_file_url = "http://bit.ly/2L18jua"
@@ -77,7 +78,7 @@ def test_de_json(self, offline_bot, animation):
             "file_unique_id": self.animation_file_unique_id,
             "width": self.width,
             "height": self.height,
-            "duration": self.duration,
+            "duration": self.duration.total_seconds(),
             "thumbnail": animation.thumbnail.to_dict(),
             "file_name": self.file_name,
             "mime_type": self.mime_type,
@@ -90,6 +91,7 @@ def test_de_json(self, offline_bot, animation):
         assert animation.file_name == self.file_name
         assert animation.mime_type == self.mime_type
         assert animation.file_size == self.file_size
+        assert animation._duration == self.duration
 
     def test_to_dict(self, animation):
         animation_dict = animation.to_dict()
@@ -99,12 +101,31 @@ def test_to_dict(self, animation):
         assert animation_dict["file_unique_id"] == animation.file_unique_id
         assert animation_dict["width"] == animation.width
         assert animation_dict["height"] == animation.height
-        assert animation_dict["duration"] == animation.duration
+        assert animation_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(animation_dict["duration"], int)
         assert animation_dict["thumbnail"] == animation.thumbnail.to_dict()
         assert animation_dict["file_name"] == animation.file_name
         assert animation_dict["mime_type"] == animation.mime_type
         assert animation_dict["file_size"] == animation.file_size
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, animation):
+        if PTB_TIMEDELTA:
+            assert animation.duration == self.duration
+            assert isinstance(animation.duration, dtm.timedelta)
+        else:
+            assert animation.duration == int(self.duration.total_seconds())
+            assert isinstance(animation.duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, animation):
+        animation.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = Animation(
             self.animation_file_id,
diff --git a/tests/_files/test_audio.py b/tests/_files/test_audio.py
index 78112058cdd..47d8dff9c2f 100644
--- a/tests/_files/test_audio.py
+++ b/tests/_files/test_audio.py
@@ -28,6 +28,7 @@
 from telegram.error import BadRequest, TelegramError
 from telegram.helpers import escape_markdown
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.bot_method_checks import (
     check_defaults_handling,
     check_shortcut_call,
@@ -43,7 +44,7 @@ class AudioTestBase:
     performer = "Leandro Toledo"
     title = "Teste"
     file_name = "telegram.mp3"
-    duration = 3
+    duration = dtm.timedelta(seconds=3)
     # audio_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.mp3'
     # Shortened link, the above one is cached with the wrong duration.
     audio_file_url = "https://goo.gl/3En24v"
@@ -71,7 +72,7 @@ def test_creation(self, audio):
         assert audio.file_unique_id
 
     def test_expected_values(self, audio):
-        assert audio.duration == self.duration
+        assert audio._duration == self.duration
         assert audio.performer is None
         assert audio.title is None
         assert audio.mime_type == self.mime_type
@@ -84,7 +85,7 @@ def test_de_json(self, offline_bot, audio):
         json_dict = {
             "file_id": self.audio_file_id,
             "file_unique_id": self.audio_file_unique_id,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
             "performer": self.performer,
             "title": self.title,
             "file_name": self.file_name,
@@ -97,7 +98,7 @@ def test_de_json(self, offline_bot, audio):
 
         assert json_audio.file_id == self.audio_file_id
         assert json_audio.file_unique_id == self.audio_file_unique_id
-        assert json_audio.duration == self.duration
+        assert json_audio._duration == self.duration
         assert json_audio.performer == self.performer
         assert json_audio.title == self.title
         assert json_audio.file_name == self.file_name
@@ -111,11 +112,30 @@ def test_to_dict(self, audio):
         assert isinstance(audio_dict, dict)
         assert audio_dict["file_id"] == audio.file_id
         assert audio_dict["file_unique_id"] == audio.file_unique_id
-        assert audio_dict["duration"] == audio.duration
+        assert audio_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(audio_dict["duration"], int)
         assert audio_dict["mime_type"] == audio.mime_type
         assert audio_dict["file_size"] == audio.file_size
         assert audio_dict["file_name"] == audio.file_name
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, audio):
+        if PTB_TIMEDELTA:
+            assert audio.duration == self.duration
+            assert isinstance(audio.duration, dtm.timedelta)
+        else:
+            assert audio.duration == int(self.duration.total_seconds())
+            assert isinstance(audio.duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, audio):
+        audio.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self, audio):
         a = Audio(audio.file_id, audio.file_unique_id, audio.duration)
         b = Audio("", audio.file_unique_id, audio.duration)
@@ -237,7 +257,7 @@ async def test_send_all_args(self, bot, chat_id, audio_file, thumb_file, duratio
         assert isinstance(message.audio.file_unique_id, str)
         assert message.audio.file_unique_id is not None
         assert message.audio.file_id is not None
-        assert message.audio.duration == self.duration
+        assert message.audio._duration == self.duration
         assert message.audio.performer == self.performer
         assert message.audio.title == self.title
         assert message.audio.file_name == self.file_name
diff --git a/tests/_files/test_inputmedia.py b/tests/_files/test_inputmedia.py
index a077c309cc5..08bdf3428a3 100644
--- a/tests/_files/test_inputmedia.py
+++ b/tests/_files/test_inputmedia.py
@@ -18,6 +18,7 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 import asyncio
 import copy
+import datetime as dtm
 from collections.abc import Sequence
 from typing import Optional
 
@@ -40,6 +41,7 @@
 from telegram.constants import InputMediaType, ParseMode
 from telegram.error import BadRequest
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.files import data_file
 from tests.auxil.networking import expect_bad_request
 from tests.auxil.slots import mro_slots
@@ -147,7 +149,7 @@ class InputMediaVideoTestBase:
     caption = "My Caption"
     width = 3
     height = 4
-    duration = 5
+    duration = dtm.timedelta(seconds=5)
     start_timestamp = 3
     parse_mode = "HTML"
     supports_streaming = True
@@ -169,7 +171,7 @@ def test_expected_values(self, input_media_video):
         assert input_media_video.caption == self.caption
         assert input_media_video.width == self.width
         assert input_media_video.height == self.height
-        assert input_media_video.duration == self.duration
+        assert input_media_video._duration == self.duration
         assert input_media_video.parse_mode == self.parse_mode
         assert input_media_video.caption_entities == tuple(self.caption_entities)
         assert input_media_video.supports_streaming == self.supports_streaming
@@ -190,7 +192,8 @@ def test_to_dict(self, input_media_video):
         assert input_media_video_dict["caption"] == input_media_video.caption
         assert input_media_video_dict["width"] == input_media_video.width
         assert input_media_video_dict["height"] == input_media_video.height
-        assert input_media_video_dict["duration"] == input_media_video.duration
+        assert input_media_video_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(input_media_video_dict["duration"], int)
         assert input_media_video_dict["parse_mode"] == input_media_video.parse_mode
         assert input_media_video_dict["caption_entities"] == [
             ce.to_dict() for ce in input_media_video.caption_entities
@@ -204,7 +207,27 @@ def test_to_dict(self, input_media_video):
         assert input_media_video_dict["cover"] == input_media_video.cover
         assert input_media_video_dict["start_timestamp"] == input_media_video.start_timestamp
 
-    def test_with_video(self, video):
+    def test_time_period_properties(self, PTB_TIMEDELTA, input_media_video):
+        duration = input_media_video.duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, input_media_video):
+        input_media_video.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
+    def test_with_video(self, video, PTB_TIMEDELTA):
         # fixture found in test_video
         input_media_video = InputMediaVideo(video, caption="test 3")
         assert input_media_video.type == self.type_
@@ -324,7 +347,7 @@ class InputMediaAnimationTestBase:
     caption_entities = [MessageEntity(MessageEntity.BOLD, 0, 2)]
     width = 30
     height = 30
-    duration = 1
+    duration = dtm.timedelta(seconds=1)
     has_spoiler = True
     show_caption_above_media = True
 
@@ -345,6 +368,7 @@ def test_expected_values(self, input_media_animation):
         assert isinstance(input_media_animation.thumbnail, InputFile)
         assert input_media_animation.has_spoiler == self.has_spoiler
         assert input_media_animation.show_caption_above_media == self.show_caption_above_media
+        assert input_media_animation._duration == self.duration
 
     def test_caption_entities_always_tuple(self):
         input_media_animation = InputMediaAnimation(self.media)
@@ -361,13 +385,34 @@ def test_to_dict(self, input_media_animation):
         ]
         assert input_media_animation_dict["width"] == input_media_animation.width
         assert input_media_animation_dict["height"] == input_media_animation.height
-        assert input_media_animation_dict["duration"] == input_media_animation.duration
+        assert input_media_animation_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(input_media_animation_dict["duration"], int)
         assert input_media_animation_dict["has_spoiler"] == input_media_animation.has_spoiler
         assert (
             input_media_animation_dict["show_caption_above_media"]
             == input_media_animation.show_caption_above_media
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, input_media_animation):
+        duration = input_media_animation.duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, input_media_animation):
+        input_media_animation.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_with_animation(self, animation):
         # fixture found in test_animation
         input_media_animation = InputMediaAnimation(animation, caption="test 2")
@@ -394,7 +439,7 @@ class InputMediaAudioTestBase:
     type_ = "audio"
     media = "NOTAREALFILEID"
     caption = "My Caption"
-    duration = 3
+    duration = dtm.timedelta(seconds=3)
     performer = "performer"
     title = "title"
     parse_mode = "HTML"
@@ -412,7 +457,7 @@ def test_expected_values(self, input_media_audio):
         assert input_media_audio.type == self.type_
         assert input_media_audio.media == self.media
         assert input_media_audio.caption == self.caption
-        assert input_media_audio.duration == self.duration
+        assert input_media_audio._duration == self.duration
         assert input_media_audio.performer == self.performer
         assert input_media_audio.title == self.title
         assert input_media_audio.parse_mode == self.parse_mode
@@ -428,7 +473,9 @@ def test_to_dict(self, input_media_audio):
         assert input_media_audio_dict["type"] == input_media_audio.type
         assert input_media_audio_dict["media"] == input_media_audio.media
         assert input_media_audio_dict["caption"] == input_media_audio.caption
-        assert input_media_audio_dict["duration"] == input_media_audio.duration
+        assert isinstance(input_media_audio_dict["duration"], int)
+        assert input_media_audio_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(input_media_audio_dict["duration"], int)
         assert input_media_audio_dict["performer"] == input_media_audio.performer
         assert input_media_audio_dict["title"] == input_media_audio.title
         assert input_media_audio_dict["parse_mode"] == input_media_audio.parse_mode
@@ -436,6 +483,26 @@ def test_to_dict(self, input_media_audio):
             ce.to_dict() for ce in input_media_audio.caption_entities
         ]
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, input_media_audio):
+        duration = input_media_audio.duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, input_media_audio):
+        input_media_audio.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_with_audio(self, audio):
         # fixture found in test_audio
         input_media_audio = InputMediaAudio(audio, caption="test 3")
@@ -574,7 +641,7 @@ def test_expected_values(self, input_paid_media_video):
         assert input_paid_media_video.media == self.media
         assert input_paid_media_video.width == self.width
         assert input_paid_media_video.height == self.height
-        assert input_paid_media_video.duration == self.duration
+        assert input_paid_media_video._duration == self.duration
         assert input_paid_media_video.supports_streaming == self.supports_streaming
         assert isinstance(input_paid_media_video.thumbnail, InputFile)
         assert isinstance(input_paid_media_video.cover, InputFile)
@@ -586,7 +653,8 @@ def test_to_dict(self, input_paid_media_video):
         assert input_paid_media_video_dict["media"] == input_paid_media_video.media
         assert input_paid_media_video_dict["width"] == input_paid_media_video.width
         assert input_paid_media_video_dict["height"] == input_paid_media_video.height
-        assert input_paid_media_video_dict["duration"] == input_paid_media_video.duration
+        assert input_paid_media_video_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(input_paid_media_video_dict["duration"], int)
         assert (
             input_paid_media_video_dict["supports_streaming"]
             == input_paid_media_video.supports_streaming
@@ -598,6 +666,26 @@ def test_to_dict(self, input_paid_media_video):
             == input_paid_media_video.start_timestamp
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, input_paid_media_video):
+        duration = input_paid_media_video.duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, input_paid_media_video):
+        input_paid_media_video.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_with_video(self, video):
         # fixture found in test_video
         input_paid_media_video = InputPaidMediaVideo(video)
diff --git a/tests/_files/test_location.py b/tests/_files/test_location.py
index 5ccddbac527..30cfb20595f 100644
--- a/tests/_files/test_location.py
+++ b/tests/_files/test_location.py
@@ -25,6 +25,7 @@
 from telegram.constants import ParseMode
 from telegram.error import BadRequest
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.build_messages import make_message
 from tests.auxil.slots import mro_slots
 
@@ -45,7 +46,7 @@ class LocationTestBase:
     latitude = -23.691288
     longitude = -46.788279
     horizontal_accuracy = 999
-    live_period = 60
+    live_period = dtm.timedelta(seconds=60)
     heading = 90
     proximity_alert_radius = 50
 
@@ -61,7 +62,7 @@ def test_de_json(self, offline_bot):
             "latitude": self.latitude,
             "longitude": self.longitude,
             "horizontal_accuracy": self.horizontal_accuracy,
-            "live_period": self.live_period,
+            "live_period": int(self.live_period.total_seconds()),
             "heading": self.heading,
             "proximity_alert_radius": self.proximity_alert_radius,
         }
@@ -71,7 +72,7 @@ def test_de_json(self, offline_bot):
         assert location.latitude == self.latitude
         assert location.longitude == self.longitude
         assert location.horizontal_accuracy == self.horizontal_accuracy
-        assert location.live_period == self.live_period
+        assert location._live_period == self.live_period
         assert location.heading == self.heading
         assert location.proximity_alert_radius == self.proximity_alert_radius
 
@@ -81,10 +82,29 @@ def test_to_dict(self, location):
         assert location_dict["latitude"] == location.latitude
         assert location_dict["longitude"] == location.longitude
         assert location_dict["horizontal_accuracy"] == location.horizontal_accuracy
-        assert location_dict["live_period"] == location.live_period
+        assert location_dict["live_period"] == int(self.live_period.total_seconds())
+        assert isinstance(location_dict["live_period"], int)
         assert location["heading"] == location.heading
         assert location["proximity_alert_radius"] == location.proximity_alert_radius
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, location):
+        if PTB_TIMEDELTA:
+            assert location.live_period == self.live_period
+            assert isinstance(location.live_period, dtm.timedelta)
+        else:
+            assert location.live_period == int(self.live_period.total_seconds())
+            assert isinstance(location.live_period, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, location):
+        location.live_period
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`live_period` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = Location(self.longitude, self.latitude)
         b = Location(self.longitude, self.latitude)
diff --git a/tests/_files/test_video.py b/tests/_files/test_video.py
index d4d87122576..ee2bfd81aa1 100644
--- a/tests/_files/test_video.py
+++ b/tests/_files/test_video.py
@@ -28,6 +28,7 @@
 from telegram.error import BadRequest, TelegramError
 from telegram.helpers import escape_markdown
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.bot_method_checks import (
     check_defaults_handling,
     check_shortcut_call,
@@ -38,15 +39,22 @@
 from tests.auxil.slots import mro_slots
 
 
+@pytest.fixture(scope="module")
+def video(video):
+    with video._unfrozen():
+        video._start_timestamp = VideoTestBase.start_timestamp
+    return video
+
+
 class VideoTestBase:
     width = 360
     height = 640
-    duration = 5
+    duration = dtm.timedelta(seconds=5)
     file_size = 326534
     mime_type = "video/mp4"
     supports_streaming = True
     file_name = "telegram.mp4"
-    start_timestamp = 3
+    start_timestamp = dtm.timedelta(seconds=3)
     cover = (PhotoSize("file_id", "unique_id", 640, 360, file_size=0),)
     thumb_width = 180
     thumb_height = 320
@@ -80,9 +88,10 @@ def test_creation(self, video):
     def test_expected_values(self, video):
         assert video.width == self.width
         assert video.height == self.height
-        assert video.duration == self.duration
+        assert video._duration == self.duration
         assert video.file_size == self.file_size
         assert video.mime_type == self.mime_type
+        assert video._start_timestamp == self.start_timestamp
 
     def test_de_json(self, offline_bot):
         json_dict = {
@@ -90,11 +99,11 @@ def test_de_json(self, offline_bot):
             "file_unique_id": self.video_file_unique_id,
             "width": self.width,
             "height": self.height,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
             "mime_type": self.mime_type,
             "file_size": self.file_size,
             "file_name": self.file_name,
-            "start_timestamp": self.start_timestamp,
+            "start_timestamp": int(self.start_timestamp.total_seconds()),
             "cover": [photo_size.to_dict() for photo_size in self.cover],
         }
         json_video = Video.de_json(json_dict, offline_bot)
@@ -104,11 +113,11 @@ def test_de_json(self, offline_bot):
         assert json_video.file_unique_id == self.video_file_unique_id
         assert json_video.width == self.width
         assert json_video.height == self.height
-        assert json_video.duration == self.duration
+        assert json_video._duration == self.duration
         assert json_video.mime_type == self.mime_type
         assert json_video.file_size == self.file_size
         assert json_video.file_name == self.file_name
-        assert json_video.start_timestamp == self.start_timestamp
+        assert json_video._start_timestamp == self.start_timestamp
         assert json_video.cover == self.cover
 
     def test_to_dict(self, video):
@@ -119,10 +128,39 @@ def test_to_dict(self, video):
         assert video_dict["file_unique_id"] == video.file_unique_id
         assert video_dict["width"] == video.width
         assert video_dict["height"] == video.height
-        assert video_dict["duration"] == video.duration
+        assert video_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(video_dict["duration"], int)
         assert video_dict["mime_type"] == video.mime_type
         assert video_dict["file_size"] == video.file_size
         assert video_dict["file_name"] == video.file_name
+        assert video_dict["start_timestamp"] == int(self.start_timestamp.total_seconds())
+        assert isinstance(video_dict["start_timestamp"], int)
+
+    def test_time_period_properties(self, PTB_TIMEDELTA, video):
+        if PTB_TIMEDELTA:
+            assert video.duration == self.duration
+            assert isinstance(video.duration, dtm.timedelta)
+
+            assert video.start_timestamp == self.start_timestamp
+            assert isinstance(video.start_timestamp, dtm.timedelta)
+        else:
+            assert video.duration == int(self.duration.total_seconds())
+            assert isinstance(video.duration, int)
+
+            assert video.start_timestamp == int(self.start_timestamp.total_seconds())
+            assert isinstance(video.start_timestamp, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, video):
+        video.duration
+        video.start_timestamp
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 2
+            for i, attr in enumerate(["duration", "start_timestamp"]):
+                assert f"`{attr}` will be of type `datetime.timedelta`" in str(recwarn[i].message)
+                assert recwarn[i].category is PTBDeprecationWarning
 
     def test_equality(self, video):
         a = Video(video.file_id, video.file_unique_id, self.width, self.height, self.duration)
@@ -266,7 +304,7 @@ async def test_send_all_args(
         assert message.video.thumbnail.width == self.thumb_width
         assert message.video.thumbnail.height == self.thumb_height
 
-        assert message.video.start_timestamp == self.start_timestamp
+        assert message.video._start_timestamp == self.start_timestamp
 
         assert isinstance(message.video.cover, tuple)
         assert isinstance(message.video.cover[0], PhotoSize)
diff --git a/tests/_files/test_videonote.py b/tests/_files/test_videonote.py
index 5edab597806..40f853bca52 100644
--- a/tests/_files/test_videonote.py
+++ b/tests/_files/test_videonote.py
@@ -27,6 +27,7 @@
 from telegram.constants import ParseMode
 from telegram.error import BadRequest, TelegramError
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.bot_method_checks import (
     check_defaults_handling,
     check_shortcut_call,
@@ -51,7 +52,7 @@ async def video_note(bot, chat_id):
 
 class VideoNoteTestBase:
     length = 240
-    duration = 3
+    duration = dtm.timedelta(seconds=3)
     file_size = 132084
     thumb_width = 240
     thumb_height = 240
@@ -81,17 +82,12 @@ def test_creation(self, video_note):
         assert video_note.thumbnail.file_id
         assert video_note.thumbnail.file_unique_id
 
-    def test_expected_values(self, video_note):
-        assert video_note.length == self.length
-        assert video_note.duration == self.duration
-        assert video_note.file_size == self.file_size
-
     def test_de_json(self, offline_bot):
         json_dict = {
             "file_id": self.videonote_file_id,
             "file_unique_id": self.videonote_file_unique_id,
             "length": self.length,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
             "file_size": self.file_size,
         }
         json_video_note = VideoNote.de_json(json_dict, offline_bot)
@@ -100,7 +96,7 @@ def test_de_json(self, offline_bot):
         assert json_video_note.file_id == self.videonote_file_id
         assert json_video_note.file_unique_id == self.videonote_file_unique_id
         assert json_video_note.length == self.length
-        assert json_video_note.duration == self.duration
+        assert json_video_note._duration == self.duration
         assert json_video_note.file_size == self.file_size
 
     def test_to_dict(self, video_note):
@@ -110,9 +106,28 @@ def test_to_dict(self, video_note):
         assert video_note_dict["file_id"] == video_note.file_id
         assert video_note_dict["file_unique_id"] == video_note.file_unique_id
         assert video_note_dict["length"] == video_note.length
-        assert video_note_dict["duration"] == video_note.duration
+        assert video_note_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(video_note_dict["duration"], int)
         assert video_note_dict["file_size"] == video_note.file_size
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, video_note):
+        if PTB_TIMEDELTA:
+            assert video_note.duration == self.duration
+            assert isinstance(video_note.duration, dtm.timedelta)
+        else:
+            assert video_note.duration == int(self.duration.total_seconds())
+            assert isinstance(video_note.duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, video_note):
+        video_note.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self, video_note):
         a = VideoNote(video_note.file_id, video_note.file_unique_id, self.length, self.duration)
         b = VideoNote("", video_note.file_unique_id, self.length, self.duration)
diff --git a/tests/_files/test_voice.py b/tests/_files/test_voice.py
index c06b1218139..62fdb4e79f8 100644
--- a/tests/_files/test_voice.py
+++ b/tests/_files/test_voice.py
@@ -28,6 +28,7 @@
 from telegram.error import BadRequest, TelegramError
 from telegram.helpers import escape_markdown
 from telegram.request import RequestData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.bot_method_checks import (
     check_defaults_handling,
     check_shortcut_call,
@@ -51,7 +52,7 @@ async def voice(bot, chat_id):
 
 
 class VoiceTestBase:
-    duration = 3
+    duration = dtm.timedelta(seconds=3)
     mime_type = "audio/ogg"
     file_size = 9199
     caption = "Test *voice*"
@@ -75,7 +76,7 @@ async def test_creation(self, voice):
         assert voice.file_unique_id
 
     def test_expected_values(self, voice):
-        assert voice.duration == self.duration
+        assert voice._duration == self.duration
         assert voice.mime_type == self.mime_type
         assert voice.file_size == self.file_size
 
@@ -83,7 +84,7 @@ def test_de_json(self, offline_bot):
         json_dict = {
             "file_id": self.voice_file_id,
             "file_unique_id": self.voice_file_unique_id,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
             "mime_type": self.mime_type,
             "file_size": self.file_size,
         }
@@ -92,7 +93,7 @@ def test_de_json(self, offline_bot):
 
         assert json_voice.file_id == self.voice_file_id
         assert json_voice.file_unique_id == self.voice_file_unique_id
-        assert json_voice.duration == self.duration
+        assert json_voice._duration == self.duration
         assert json_voice.mime_type == self.mime_type
         assert json_voice.file_size == self.file_size
 
@@ -102,10 +103,29 @@ def test_to_dict(self, voice):
         assert isinstance(voice_dict, dict)
         assert voice_dict["file_id"] == voice.file_id
         assert voice_dict["file_unique_id"] == voice.file_unique_id
-        assert voice_dict["duration"] == voice.duration
+        assert voice_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(voice_dict["duration"], int)
         assert voice_dict["mime_type"] == voice.mime_type
         assert voice_dict["file_size"] == voice.file_size
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, voice):
+        if PTB_TIMEDELTA:
+            assert voice.duration == self.duration
+            assert isinstance(voice.duration, dtm.timedelta)
+        else:
+            assert voice.duration == int(self.duration.total_seconds())
+            assert isinstance(voice.duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, voice):
+        voice.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self, voice):
         a = Voice(voice.file_id, voice.file_unique_id, self.duration)
         b = Voice("", voice.file_unique_id, self.duration)
diff --git a/tests/_inline/test_inlinequeryresultaudio.py b/tests/_inline/test_inlinequeryresultaudio.py
index 4c781655910..17871fa854d 100644
--- a/tests/_inline/test_inlinequeryresultaudio.py
+++ b/tests/_inline/test_inlinequeryresultaudio.py
@@ -17,6 +17,8 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -27,6 +29,7 @@
     InputTextMessageContent,
     MessageEntity,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -52,7 +55,7 @@ class InlineQueryResultAudioTestBase:
     audio_url = "audio url"
     title = "title"
     performer = "performer"
-    audio_duration = "audio_duration"
+    audio_duration = dtm.timedelta(seconds=10)
     caption = "caption"
     parse_mode = "Markdown"
     caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)]
@@ -73,7 +76,7 @@ def test_expected_values(self, inline_query_result_audio):
         assert inline_query_result_audio.audio_url == self.audio_url
         assert inline_query_result_audio.title == self.title
         assert inline_query_result_audio.performer == self.performer
-        assert inline_query_result_audio.audio_duration == self.audio_duration
+        assert inline_query_result_audio._audio_duration == self.audio_duration
         assert inline_query_result_audio.caption == self.caption
         assert inline_query_result_audio.parse_mode == self.parse_mode
         assert inline_query_result_audio.caption_entities == tuple(self.caption_entities)
@@ -92,10 +95,10 @@ def test_to_dict(self, inline_query_result_audio):
         assert inline_query_result_audio_dict["audio_url"] == inline_query_result_audio.audio_url
         assert inline_query_result_audio_dict["title"] == inline_query_result_audio.title
         assert inline_query_result_audio_dict["performer"] == inline_query_result_audio.performer
-        assert (
-            inline_query_result_audio_dict["audio_duration"]
-            == inline_query_result_audio.audio_duration
+        assert inline_query_result_audio_dict["audio_duration"] == int(
+            self.audio_duration.total_seconds()
         )
+        assert isinstance(inline_query_result_audio_dict["audio_duration"], int)
         assert inline_query_result_audio_dict["caption"] == inline_query_result_audio.caption
         assert inline_query_result_audio_dict["parse_mode"] == inline_query_result_audio.parse_mode
         assert inline_query_result_audio_dict["caption_entities"] == [
@@ -114,6 +117,28 @@ def test_caption_entities_always_tuple(self):
         inline_query_result_audio = InlineQueryResultAudio(self.id_, self.audio_url, self.title)
         assert inline_query_result_audio.caption_entities == ()
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_audio):
+        audio_duration = inline_query_result_audio.audio_duration
+
+        if PTB_TIMEDELTA:
+            assert audio_duration == self.audio_duration
+            assert isinstance(audio_duration, dtm.timedelta)
+        else:
+            assert audio_duration == int(self.audio_duration.total_seconds())
+            assert isinstance(audio_duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, inline_query_result_audio):
+        inline_query_result_audio.audio_duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`audio_duration` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InlineQueryResultAudio(self.id_, self.audio_url, self.title)
         b = InlineQueryResultAudio(self.id_, self.title, self.title)
diff --git a/tests/_inline/test_inlinequeryresultgif.py b/tests/_inline/test_inlinequeryresultgif.py
index 878b9b61d3c..2806e895623 100644
--- a/tests/_inline/test_inlinequeryresultgif.py
+++ b/tests/_inline/test_inlinequeryresultgif.py
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -26,6 +28,7 @@
     InputTextMessageContent,
     MessageEntity,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -55,7 +58,7 @@ class InlineQueryResultGifTestBase:
     gif_url = "gif url"
     gif_width = 10
     gif_height = 15
-    gif_duration = 1
+    gif_duration = dtm.timedelta(seconds=1)
     thumbnail_url = "thumb url"
     thumbnail_mime_type = "image/jpeg"
     title = "title"
@@ -84,7 +87,7 @@ def test_expected_values(self, inline_query_result_gif):
         assert inline_query_result_gif.gif_url == self.gif_url
         assert inline_query_result_gif.gif_width == self.gif_width
         assert inline_query_result_gif.gif_height == self.gif_height
-        assert inline_query_result_gif.gif_duration == self.gif_duration
+        assert inline_query_result_gif._gif_duration == self.gif_duration
         assert inline_query_result_gif.thumbnail_url == self.thumbnail_url
         assert inline_query_result_gif.thumbnail_mime_type == self.thumbnail_mime_type
         assert inline_query_result_gif.title == self.title
@@ -107,7 +110,10 @@ def test_to_dict(self, inline_query_result_gif):
         assert inline_query_result_gif_dict["gif_url"] == inline_query_result_gif.gif_url
         assert inline_query_result_gif_dict["gif_width"] == inline_query_result_gif.gif_width
         assert inline_query_result_gif_dict["gif_height"] == inline_query_result_gif.gif_height
-        assert inline_query_result_gif_dict["gif_duration"] == inline_query_result_gif.gif_duration
+        assert inline_query_result_gif_dict["gif_duration"] == int(
+            self.gif_duration.total_seconds()
+        )
+        assert isinstance(inline_query_result_gif_dict["gif_duration"], int)
         assert (
             inline_query_result_gif_dict["thumbnail_url"] == inline_query_result_gif.thumbnail_url
         )
@@ -134,6 +140,26 @@ def test_to_dict(self, inline_query_result_gif):
             == inline_query_result_gif.show_caption_above_media
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_gif):
+        gif_duration = inline_query_result_gif.gif_duration
+
+        if PTB_TIMEDELTA:
+            assert gif_duration == self.gif_duration
+            assert isinstance(gif_duration, dtm.timedelta)
+        else:
+            assert gif_duration == int(self.gif_duration.total_seconds())
+            assert isinstance(gif_duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, inline_query_result_gif):
+        inline_query_result_gif.gif_duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`gif_duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url)
         b = InlineQueryResultGif(self.id_, self.gif_url, self.thumbnail_url)
diff --git a/tests/_inline/test_inlinequeryresultlocation.py b/tests/_inline/test_inlinequeryresultlocation.py
index db9c64cfd10..a9471f0d55d 100644
--- a/tests/_inline/test_inlinequeryresultlocation.py
+++ b/tests/_inline/test_inlinequeryresultlocation.py
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -25,6 +27,7 @@
     InlineQueryResultVoice,
     InputTextMessageContent,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -54,7 +57,7 @@ class InlineQueryResultLocationTestBase:
     longitude = 1.0
     title = "title"
     horizontal_accuracy = 999
-    live_period = 70
+    live_period = dtm.timedelta(seconds=70)
     heading = 90
     proximity_alert_radius = 1000
     thumbnail_url = "thumb url"
@@ -77,7 +80,7 @@ def test_expected_values(self, inline_query_result_location):
         assert inline_query_result_location.latitude == self.latitude
         assert inline_query_result_location.longitude == self.longitude
         assert inline_query_result_location.title == self.title
-        assert inline_query_result_location.live_period == self.live_period
+        assert inline_query_result_location._live_period == self.live_period
         assert inline_query_result_location.thumbnail_url == self.thumbnail_url
         assert inline_query_result_location.thumbnail_width == self.thumbnail_width
         assert inline_query_result_location.thumbnail_height == self.thumbnail_height
@@ -104,10 +107,10 @@ def test_to_dict(self, inline_query_result_location):
             == inline_query_result_location.longitude
         )
         assert inline_query_result_location_dict["title"] == inline_query_result_location.title
-        assert (
-            inline_query_result_location_dict["live_period"]
-            == inline_query_result_location.live_period
+        assert inline_query_result_location_dict["live_period"] == int(
+            self.live_period.total_seconds()
         )
+        assert isinstance(inline_query_result_location_dict["live_period"], int)
         assert (
             inline_query_result_location_dict["thumbnail_url"]
             == inline_query_result_location.thumbnail_url
@@ -138,6 +141,28 @@ def test_to_dict(self, inline_query_result_location):
             == inline_query_result_location.proximity_alert_radius
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_location):
+        live_period = inline_query_result_location.live_period
+
+        if PTB_TIMEDELTA:
+            assert live_period == self.live_period
+            assert isinstance(live_period, dtm.timedelta)
+        else:
+            assert live_period == int(self.live_period.total_seconds())
+            assert isinstance(live_period, int)
+
+    def test_time_period_int_deprecated(
+        self, recwarn, PTB_TIMEDELTA, inline_query_result_location
+    ):
+        inline_query_result_location.live_period
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`live_period` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InlineQueryResultLocation(self.id_, self.longitude, self.latitude, self.title)
         b = InlineQueryResultLocation(self.id_, self.longitude, self.latitude, self.title)
diff --git a/tests/_inline/test_inlinequeryresultmpeg4gif.py b/tests/_inline/test_inlinequeryresultmpeg4gif.py
index 03b6ca991d1..4c8291c4e5a 100644
--- a/tests/_inline/test_inlinequeryresultmpeg4gif.py
+++ b/tests/_inline/test_inlinequeryresultmpeg4gif.py
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -26,6 +28,7 @@
     InputTextMessageContent,
     MessageEntity,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -55,7 +58,7 @@ class InlineQueryResultMpeg4GifTestBase:
     mpeg4_url = "mpeg4 url"
     mpeg4_width = 10
     mpeg4_height = 15
-    mpeg4_duration = 1
+    mpeg4_duration = dtm.timedelta(seconds=1)
     thumbnail_url = "thumb url"
     thumbnail_mime_type = "image/jpeg"
     title = "title"
@@ -80,7 +83,7 @@ def test_expected_values(self, inline_query_result_mpeg4_gif):
         assert inline_query_result_mpeg4_gif.mpeg4_url == self.mpeg4_url
         assert inline_query_result_mpeg4_gif.mpeg4_width == self.mpeg4_width
         assert inline_query_result_mpeg4_gif.mpeg4_height == self.mpeg4_height
-        assert inline_query_result_mpeg4_gif.mpeg4_duration == self.mpeg4_duration
+        assert inline_query_result_mpeg4_gif._mpeg4_duration == self.mpeg4_duration
         assert inline_query_result_mpeg4_gif.thumbnail_url == self.thumbnail_url
         assert inline_query_result_mpeg4_gif.thumbnail_mime_type == self.thumbnail_mime_type
         assert inline_query_result_mpeg4_gif.title == self.title
@@ -118,10 +121,10 @@ def test_to_dict(self, inline_query_result_mpeg4_gif):
             inline_query_result_mpeg4_gif_dict["mpeg4_height"]
             == inline_query_result_mpeg4_gif.mpeg4_height
         )
-        assert (
-            inline_query_result_mpeg4_gif_dict["mpeg4_duration"]
-            == inline_query_result_mpeg4_gif.mpeg4_duration
+        assert inline_query_result_mpeg4_gif_dict["mpeg4_duration"] == int(
+            self.mpeg4_duration.total_seconds()
         )
+        assert isinstance(inline_query_result_mpeg4_gif_dict["mpeg4_duration"], int)
         assert (
             inline_query_result_mpeg4_gif_dict["thumbnail_url"]
             == inline_query_result_mpeg4_gif.thumbnail_url
@@ -154,6 +157,30 @@ def test_to_dict(self, inline_query_result_mpeg4_gif):
             == inline_query_result_mpeg4_gif.show_caption_above_media
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_mpeg4_gif):
+        mpeg4_duration = inline_query_result_mpeg4_gif.mpeg4_duration
+
+        if PTB_TIMEDELTA:
+            assert mpeg4_duration == self.mpeg4_duration
+            assert isinstance(mpeg4_duration, dtm.timedelta)
+        else:
+            assert mpeg4_duration == int(self.mpeg4_duration.total_seconds())
+            assert isinstance(mpeg4_duration, int)
+
+    def test_time_period_int_deprecated(
+        self, recwarn, PTB_TIMEDELTA, inline_query_result_mpeg4_gif
+    ):
+        inline_query_result_mpeg4_gif.mpeg4_duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`mpeg4_duration` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumbnail_url)
         b = InlineQueryResultMpeg4Gif(self.id_, self.mpeg4_url, self.thumbnail_url)
diff --git a/tests/_inline/test_inlinequeryresultvideo.py b/tests/_inline/test_inlinequeryresultvideo.py
index d165d9af3f2..dd07b9c9719 100644
--- a/tests/_inline/test_inlinequeryresultvideo.py
+++ b/tests/_inline/test_inlinequeryresultvideo.py
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -26,6 +28,7 @@
     InputTextMessageContent,
     MessageEntity,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -57,7 +60,7 @@ class InlineQueryResultVideoTestBase:
     mime_type = "mime type"
     video_width = 10
     video_height = 15
-    video_duration = 15
+    video_duration = dtm.timedelta(seconds=15)
     thumbnail_url = "thumbnail url"
     title = "title"
     caption = "caption"
@@ -83,7 +86,7 @@ def test_expected_values(self, inline_query_result_video):
         assert inline_query_result_video.mime_type == self.mime_type
         assert inline_query_result_video.video_width == self.video_width
         assert inline_query_result_video.video_height == self.video_height
-        assert inline_query_result_video.video_duration == self.video_duration
+        assert inline_query_result_video._video_duration == self.video_duration
         assert inline_query_result_video.thumbnail_url == self.thumbnail_url
         assert inline_query_result_video.title == self.title
         assert inline_query_result_video.description == self.description
@@ -118,10 +121,10 @@ def test_to_dict(self, inline_query_result_video):
             inline_query_result_video_dict["video_height"]
             == inline_query_result_video.video_height
         )
-        assert (
-            inline_query_result_video_dict["video_duration"]
-            == inline_query_result_video.video_duration
+        assert inline_query_result_video_dict["video_duration"] == int(
+            self.video_duration.total_seconds()
         )
+        assert isinstance(inline_query_result_video_dict["video_duration"], int)
         assert (
             inline_query_result_video_dict["thumbnail_url"]
             == inline_query_result_video.thumbnail_url
@@ -148,6 +151,29 @@ def test_to_dict(self, inline_query_result_video):
             == inline_query_result_video.show_caption_above_media
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_video):
+        iqrv = inline_query_result_video
+        if PTB_TIMEDELTA:
+            assert iqrv.video_duration == self.video_duration
+            assert isinstance(iqrv.video_duration, dtm.timedelta)
+        else:
+            assert iqrv.video_duration == int(self.video_duration.total_seconds())
+            assert isinstance(iqrv.video_duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, inline_query_result_video):
+        value = inline_query_result_video.video_duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+            assert isinstance(value, dtm.timedelta)
+        else:
+            assert len(recwarn) == 1
+            assert "`video_duration` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
+            assert isinstance(value, int)
+
     def test_equality(self):
         a = InlineQueryResultVideo(
             self.id_, self.video_url, self.mime_type, self.thumbnail_url, self.title
diff --git a/tests/_inline/test_inlinequeryresultvoice.py b/tests/_inline/test_inlinequeryresultvoice.py
index 01662700c74..f4e58cca371 100644
--- a/tests/_inline/test_inlinequeryresultvoice.py
+++ b/tests/_inline/test_inlinequeryresultvoice.py
@@ -16,6 +16,8 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import (
@@ -26,6 +28,7 @@
     InputTextMessageContent,
     MessageEntity,
 )
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -49,7 +52,7 @@ class InlineQueryResultVoiceTestBase:
     type_ = "voice"
     voice_url = "voice url"
     title = "title"
-    voice_duration = "voice_duration"
+    voice_duration = dtm.timedelta(seconds=10)
     caption = "caption"
     parse_mode = "HTML"
     caption_entities = [MessageEntity(MessageEntity.ITALIC, 0, 7)]
@@ -69,7 +72,7 @@ def test_expected_values(self, inline_query_result_voice):
         assert inline_query_result_voice.id == self.id_
         assert inline_query_result_voice.voice_url == self.voice_url
         assert inline_query_result_voice.title == self.title
-        assert inline_query_result_voice.voice_duration == self.voice_duration
+        assert inline_query_result_voice._voice_duration == self.voice_duration
         assert inline_query_result_voice.caption == self.caption
         assert inline_query_result_voice.parse_mode == self.parse_mode
         assert inline_query_result_voice.caption_entities == tuple(self.caption_entities)
@@ -96,10 +99,10 @@ def test_to_dict(self, inline_query_result_voice):
         assert inline_query_result_voice_dict["id"] == inline_query_result_voice.id
         assert inline_query_result_voice_dict["voice_url"] == inline_query_result_voice.voice_url
         assert inline_query_result_voice_dict["title"] == inline_query_result_voice.title
-        assert (
-            inline_query_result_voice_dict["voice_duration"]
-            == inline_query_result_voice.voice_duration
+        assert inline_query_result_voice_dict["voice_duration"] == int(
+            self.voice_duration.total_seconds()
         )
+        assert isinstance(inline_query_result_voice_dict["voice_duration"], int)
         assert inline_query_result_voice_dict["caption"] == inline_query_result_voice.caption
         assert inline_query_result_voice_dict["parse_mode"] == inline_query_result_voice.parse_mode
         assert inline_query_result_voice_dict["caption_entities"] == [
@@ -114,6 +117,28 @@ def test_to_dict(self, inline_query_result_voice):
             == inline_query_result_voice.reply_markup.to_dict()
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, inline_query_result_voice):
+        voice_duration = inline_query_result_voice.voice_duration
+
+        if PTB_TIMEDELTA:
+            assert voice_duration == self.voice_duration
+            assert isinstance(voice_duration, dtm.timedelta)
+        else:
+            assert voice_duration == int(self.voice_duration.total_seconds())
+            assert isinstance(voice_duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, inline_query_result_voice):
+        inline_query_result_voice.voice_duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`voice_duration` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InlineQueryResultVoice(self.id_, self.voice_url, self.title)
         b = InlineQueryResultVoice(self.id_, self.voice_url, self.title)
diff --git a/tests/_inline/test_inputlocationmessagecontent.py b/tests/_inline/test_inputlocationmessagecontent.py
index 05e86086852..1fd79ee9ad0 100644
--- a/tests/_inline/test_inputlocationmessagecontent.py
+++ b/tests/_inline/test_inputlocationmessagecontent.py
@@ -16,9 +16,12 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
+
 import pytest
 
 from telegram import InputLocationMessageContent, Location
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -37,7 +40,7 @@ def input_location_message_content():
 class InputLocationMessageContentTestBase:
     latitude = -23.691288
     longitude = -46.788279
-    live_period = 80
+    live_period = dtm.timedelta(seconds=80)
     horizontal_accuracy = 50.5
     heading = 90
     proximity_alert_radius = 999
@@ -53,7 +56,7 @@ def test_slot_behaviour(self, input_location_message_content):
     def test_expected_values(self, input_location_message_content):
         assert input_location_message_content.longitude == self.longitude
         assert input_location_message_content.latitude == self.latitude
-        assert input_location_message_content.live_period == self.live_period
+        assert input_location_message_content._live_period == self.live_period
         assert input_location_message_content.horizontal_accuracy == self.horizontal_accuracy
         assert input_location_message_content.heading == self.heading
         assert input_location_message_content.proximity_alert_radius == self.proximity_alert_radius
@@ -70,10 +73,10 @@ def test_to_dict(self, input_location_message_content):
             input_location_message_content_dict["longitude"]
             == input_location_message_content.longitude
         )
-        assert (
-            input_location_message_content_dict["live_period"]
-            == input_location_message_content.live_period
+        assert input_location_message_content_dict["live_period"] == int(
+            self.live_period.total_seconds()
         )
+        assert isinstance(input_location_message_content_dict["live_period"], int)
         assert (
             input_location_message_content_dict["horizontal_accuracy"]
             == input_location_message_content.horizontal_accuracy
@@ -87,6 +90,28 @@ def test_to_dict(self, input_location_message_content):
             == input_location_message_content.proximity_alert_radius
         )
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, input_location_message_content):
+        live_period = input_location_message_content.live_period
+
+        if PTB_TIMEDELTA:
+            assert live_period == self.live_period
+            assert isinstance(live_period, dtm.timedelta)
+        else:
+            assert live_period == int(self.live_period.total_seconds())
+            assert isinstance(live_period, int)
+
+    def test_time_period_int_deprecated(
+        self, recwarn, PTB_TIMEDELTA, input_location_message_content
+    ):
+        input_location_message_content.live_period
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`live_period` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = InputLocationMessageContent(123, 456, 70)
         b = InputLocationMessageContent(123, 456, 90)
diff --git a/tests/_utils/test_datetime.py b/tests/_utils/test_datetime.py
index dfcaca67587..8628f0c109f 100644
--- a/tests/_utils/test_datetime.py
+++ b/tests/_utils/test_datetime.py
@@ -192,3 +192,20 @@ def test_extract_tzinfo_from_defaults(self, tz_bot, bot, raw_bot):
         assert tg_dtm.extract_tzinfo_from_defaults(tz_bot) == tz_bot.defaults.tzinfo
         assert tg_dtm.extract_tzinfo_from_defaults(bot) is None
         assert tg_dtm.extract_tzinfo_from_defaults(raw_bot) is None
+
+    @pytest.mark.parametrize(
+        ("arg", "timedelta_result", "number_result"),
+        [
+            (None, None, None),
+            (dtm.timedelta(seconds=10), dtm.timedelta(seconds=10), 10),
+            (dtm.timedelta(seconds=10.5), dtm.timedelta(seconds=10.5), 10.5),
+        ],
+    )
+    def test_get_timedelta_value(self, PTB_TIMEDELTA, arg, timedelta_result, number_result):
+        result = tg_dtm.get_timedelta_value(arg, attribute="")
+
+        if PTB_TIMEDELTA:
+            assert result == timedelta_result
+        else:
+            assert result == number_result
+            assert type(result) is type(number_result)
diff --git a/tests/conftest.py b/tests/conftest.py
index 935daada498..9c1e6397c50 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -18,6 +18,7 @@
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 import asyncio
 import logging
+import os
 import sys
 import zoneinfo
 from pathlib import Path
@@ -40,7 +41,12 @@
 from tests.auxil.build_messages import DATE, make_message
 from tests.auxil.ci_bots import BOT_INFO_PROVIDER, JOB_INDEX
 from tests.auxil.constants import PRIVATE_KEY, TEST_TOPIC_ICON_COLOR, TEST_TOPIC_NAME
-from tests.auxil.envvars import GITHUB_ACTIONS, RUN_TEST_OFFICIAL, TEST_WITH_OPT_DEPS
+from tests.auxil.envvars import (
+    GITHUB_ACTIONS,
+    RUN_TEST_OFFICIAL,
+    TEST_WITH_OPT_DEPS,
+    env_var_2_bool,
+)
 from tests.auxil.files import data_file
 from tests.auxil.networking import NonchalantHttpxRequest
 from tests.auxil.pytest_classes import PytestBot, make_bot
@@ -129,6 +135,18 @@ def _disallow_requests_in_without_request_tests(request):
             )
 
 
+@pytest.fixture(scope="module", params=["true", "false", None])
+def PTB_TIMEDELTA(request):
+    # Here we manually use monkeypatch to give this fixture module scope
+    monkeypatch = pytest.MonkeyPatch()
+    if request.param is not None:
+        monkeypatch.setenv("PTB_TIMEDELTA", request.param)
+    else:
+        monkeypatch.delenv("PTB_TIMEDELTA", raising=False)
+    yield env_var_2_bool(os.getenv("PTB_TIMEDELTA"))
+    monkeypatch.undo()
+
+
 # Redefine the event_loop fixture to have a session scope. Otherwise `bot` fixture can't be
 # session. See https://github.com/pytest-dev/pytest-asyncio/issues/68 for more details.
 @pytest.fixture(scope="session")
diff --git a/tests/ext/test_applicationbuilder.py b/tests/ext/test_applicationbuilder.py
index 15e85b6416e..bfbce15dd93 100644
--- a/tests/ext/test_applicationbuilder.py
+++ b/tests/ext/test_applicationbuilder.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 import asyncio
+import datetime as dtm
 import inspect
 from dataclasses import dataclass
 from http import HTTPStatus
@@ -576,9 +577,12 @@ def test_no_job_queue(self, bot, builder):
             (None, None, 0),
             (1, None, 1),
             (None, 1, 1),
+            (None, dtm.timedelta(seconds=1), 1),
             (DEFAULT_NONE, None, 10),
             (DEFAULT_NONE, 1, 11),
+            (DEFAULT_NONE, dtm.timedelta(seconds=1), 11),
             (1, 2, 3),
+            (1, dtm.timedelta(seconds=2), 3),
         ],
     )
     async def test_get_updates_read_timeout_value_passing(
diff --git a/tests/ext/test_updater.py b/tests/ext/test_updater.py
index 147fc6128df..92a2d65ce7d 100644
--- a/tests/ext/test_updater.py
+++ b/tests/ext/test_updater.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 import asyncio
+import datetime as dtm
 import logging
 import platform
 from collections import defaultdict
@@ -294,7 +295,7 @@ async def test_polling_mark_updates_as_read(self, monkeypatch, updater, caplog):
         tracking_flag = False
         received_kwargs = {}
         expected_kwargs = {
-            "timeout": 0,
+            "timeout": dtm.timedelta(seconds=0),
             "allowed_updates": "allowed_updates",
         }
 
@@ -416,7 +417,7 @@ async def test_start_polling_get_updates_parameters(self, updater, monkeypatch):
         on_stop_flag = False
 
         expected = {
-            "timeout": 10,
+            "timeout": dtm.timedelta(seconds=10),
             "allowed_updates": None,
             "api_kwargs": None,
         }
@@ -456,14 +457,14 @@ async def get_updates(*args, **kwargs):
             on_stop_flag = False
 
             expected = {
-                "timeout": 42,
+                "timeout": dtm.timedelta(seconds=42),
                 "allowed_updates": ["message"],
                 "api_kwargs": None,
             }
 
             await update_queue.put(Update(update_id=2))
             await updater.start_polling(
-                timeout=42,
+                timeout=dtm.timedelta(seconds=42),
                 allowed_updates=["message"],
             )
             await update_queue.join()
diff --git a/tests/request/test_request.py b/tests/request/test_request.py
index 1672b8fb64e..45d18ef0c59 100644
--- a/tests/request/test_request.py
+++ b/tests/request/test_request.py
@@ -19,6 +19,7 @@
 """Here we run tests directly with HTTPXRequest because that's easier than providing dummy
 implementations for BaseRequest and we want to test HTTPXRequest anyway."""
 import asyncio
+import datetime as dtm
 import json
 import logging
 from collections import defaultdict
@@ -244,7 +245,7 @@ async def test_chat_migrated(self, monkeypatch, httpx_request: HTTPXRequest):
 
         assert exc_info.value.new_chat_id == 123
 
-    async def test_retry_after(self, monkeypatch, httpx_request: HTTPXRequest):
+    async def test_retry_after(self, monkeypatch, httpx_request: HTTPXRequest, PTB_TIMEDELTA):
         server_response = b'{"ok": "False", "parameters": {"retry_after": 42}}'
 
         monkeypatch.setattr(
@@ -253,10 +254,12 @@ async def test_retry_after(self, monkeypatch, httpx_request: HTTPXRequest):
             mocker_factory(response=server_response, return_code=HTTPStatus.BAD_REQUEST),
         )
 
-        with pytest.raises(RetryAfter, match="Retry in 42") as exc_info:
+        with pytest.raises(
+            RetryAfter, match="Retry in " + "0:00:42" if PTB_TIMEDELTA else "42"
+        ) as exc_info:
             await httpx_request.post(None, None, None)
 
-        assert exc_info.value.retry_after == 42
+        assert exc_info.value.retry_after == (dtm.timdelta(seconds=42) if PTB_TIMEDELTA else 42)
 
     async def test_unknown_request_params(self, monkeypatch, httpx_request: HTTPXRequest):
         server_response = b'{"ok": "False", "parameters": {"unknown": "42"}}'
diff --git a/tests/test_bot.py b/tests/test_bot.py
index 16c878dd29c..4e78cd0a449 100644
--- a/tests/test_bot.py
+++ b/tests/test_bot.py
@@ -3266,9 +3266,10 @@ async def test_edit_reply_markup_inline(self):
         pass
 
     # TODO: Actually send updates to the test bot so this can be tested properly
-    async def test_get_updates(self, bot):
+    @pytest.mark.parametrize("timeout", [1, dtm.timedelta(seconds=1)])
+    async def test_get_updates(self, bot, timeout):
         await bot.delete_webhook()  # make sure there is no webhook set if webhook tests failed
-        updates = await bot.get_updates(timeout=1)
+        updates = await bot.get_updates(timeout=timeout)
 
         assert isinstance(updates, tuple)
         if updates:
@@ -3280,9 +3281,12 @@ async def test_get_updates(self, bot):
             (None, None, 0),
             (1, None, 1),
             (None, 1, 1),
+            (None, dtm.timedelta(seconds=1), 1),
             (DEFAULT_NONE, None, 10),
             (DEFAULT_NONE, 1, 11),
+            (DEFAULT_NONE, dtm.timedelta(seconds=1), 11),
             (1, 2, 3),
+            (1, dtm.timedelta(seconds=2), 3),
         ],
     )
     async def test_get_updates_read_timeout_value_passing(
diff --git a/tests/test_chatfullinfo.py b/tests/test_chatfullinfo.py
index dff26aa7398..52444fcbd34 100644
--- a/tests/test_chatfullinfo.py
+++ b/tests/test_chatfullinfo.py
@@ -55,6 +55,7 @@ def chat_full_info(bot):
         can_set_sticker_set=ChatFullInfoTestBase.can_set_sticker_set,
         permissions=ChatFullInfoTestBase.permissions,
         slow_mode_delay=ChatFullInfoTestBase.slow_mode_delay,
+        message_auto_delete_time=ChatFullInfoTestBase.message_auto_delete_time,
         bio=ChatFullInfoTestBase.bio,
         linked_chat_id=ChatFullInfoTestBase.linked_chat_id,
         location=ChatFullInfoTestBase.location,
@@ -106,7 +107,8 @@ class ChatFullInfoTestBase:
         can_change_info=False,
         can_invite_users=True,
     )
-    slow_mode_delay = 30
+    slow_mode_delay = dtm.timedelta(seconds=30)
+    message_auto_delete_time = dtm.timedelta(60)
     bio = "I'm a Barbie Girl in a Barbie World"
     linked_chat_id = 11880
     location = ChatLocation(Location(123, 456), "Barbie World")
@@ -168,7 +170,8 @@ def test_de_json(self, offline_bot):
             "sticker_set_name": self.sticker_set_name,
             "can_set_sticker_set": self.can_set_sticker_set,
             "permissions": self.permissions.to_dict(),
-            "slow_mode_delay": self.slow_mode_delay,
+            "slow_mode_delay": self.slow_mode_delay.total_seconds(),
+            "message_auto_delete_time": self.message_auto_delete_time.total_seconds(),
             "bio": self.bio,
             "business_intro": self.business_intro.to_dict(),
             "business_location": self.business_location.to_dict(),
@@ -201,6 +204,7 @@ def test_de_json(self, offline_bot):
             "last_name": self.last_name,
             "can_send_paid_media": self.can_send_paid_media,
         }
+
         cfi = ChatFullInfo.de_json(json_dict, offline_bot)
         assert cfi.api_kwargs == {}
         assert cfi.id == self.id_
@@ -211,7 +215,8 @@ def test_de_json(self, offline_bot):
         assert cfi.sticker_set_name == self.sticker_set_name
         assert cfi.can_set_sticker_set == self.can_set_sticker_set
         assert cfi.permissions == self.permissions
-        assert cfi.slow_mode_delay == self.slow_mode_delay
+        assert cfi._slow_mode_delay == self.slow_mode_delay
+        assert cfi._message_auto_delete_time == self.message_auto_delete_time
         assert cfi.bio == self.bio
         assert cfi.business_intro == self.business_intro
         assert cfi.business_location == self.business_location
@@ -281,7 +286,10 @@ def test_to_dict(self, chat_full_info):
         assert cfi_dict["type"] == cfi.type
         assert cfi_dict["username"] == cfi.username
         assert cfi_dict["permissions"] == cfi.permissions.to_dict()
-        assert cfi_dict["slow_mode_delay"] == cfi.slow_mode_delay
+        assert cfi_dict["slow_mode_delay"] == int(self.slow_mode_delay.total_seconds())
+        assert cfi_dict["message_auto_delete_time"] == int(
+            self.message_auto_delete_time.total_seconds()
+        )
         assert cfi_dict["bio"] == cfi.bio
         assert cfi_dict["business_intro"] == cfi.business_intro.to_dict()
         assert cfi_dict["business_location"] == cfi.business_location.to_dict()
@@ -355,6 +363,35 @@ def test_can_send_gift_deprecation_warning(self):
         ):
             chat_full_info.can_send_gift
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, chat_full_info):
+        cfi = chat_full_info
+        if PTB_TIMEDELTA:
+            assert cfi.slow_mode_delay == self.slow_mode_delay
+            assert isinstance(cfi.slow_mode_delay, dtm.timedelta)
+
+            assert cfi.message_auto_delete_time == self.message_auto_delete_time
+            assert isinstance(cfi.message_auto_delete_time, dtm.timedelta)
+        else:
+            assert cfi.slow_mode_delay == int(self.slow_mode_delay.total_seconds())
+            assert isinstance(cfi.slow_mode_delay, int)
+
+            assert cfi.message_auto_delete_time == int(
+                self.message_auto_delete_time.total_seconds()
+            )
+            assert isinstance(cfi.message_auto_delete_time, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, chat_full_info):
+        chat_full_info.slow_mode_delay
+        chat_full_info.message_auto_delete_time
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 2
+            for i, attr in enumerate(["slow_mode_delay", "message_auto_delete_time"]):
+                assert f"`{attr}` will be of type `datetime.timedelta`" in str(recwarn[i].message)
+                assert recwarn[i].category is PTBDeprecationWarning
+
     def test_always_tuples_attributes(self):
         cfi = ChatFullInfo(
             id=123,
diff --git a/tests/test_chatinvitelink.py b/tests/test_chatinvitelink.py
index 55cfc5763a9..f111d7bf2b6 100644
--- a/tests/test_chatinvitelink.py
+++ b/tests/test_chatinvitelink.py
@@ -22,6 +22,7 @@
 
 from telegram import ChatInviteLink, User
 from telegram._utils.datetime import UTC, to_timestamp
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -56,7 +57,7 @@ class ChatInviteLinkTestBase:
     member_limit = 42
     name = "LinkName"
     pending_join_request_count = 42
-    subscription_period = 43
+    subscription_period = dtm.timedelta(seconds=43)
     subscription_price = 44
 
 
@@ -95,7 +96,7 @@ def test_de_json_all_args(self, offline_bot, creator):
             "member_limit": self.member_limit,
             "name": self.name,
             "pending_join_request_count": str(self.pending_join_request_count),
-            "subscription_period": self.subscription_period,
+            "subscription_period": int(self.subscription_period.total_seconds()),
             "subscription_price": self.subscription_price,
         }
 
@@ -112,7 +113,7 @@ def test_de_json_all_args(self, offline_bot, creator):
         assert invite_link.member_limit == self.member_limit
         assert invite_link.name == self.name
         assert invite_link.pending_join_request_count == self.pending_join_request_count
-        assert invite_link.subscription_period == self.subscription_period
+        assert invite_link._subscription_period == self.subscription_period
         assert invite_link.subscription_price == self.subscription_price
 
     def test_de_json_localization(self, tz_bot, offline_bot, raw_bot, creator):
@@ -154,9 +155,32 @@ def test_to_dict(self, invite_link):
         assert invite_link_dict["member_limit"] == self.member_limit
         assert invite_link_dict["name"] == self.name
         assert invite_link_dict["pending_join_request_count"] == self.pending_join_request_count
-        assert invite_link_dict["subscription_period"] == self.subscription_period
+        assert invite_link_dict["subscription_period"] == int(
+            self.subscription_period.total_seconds()
+        )
+        assert isinstance(invite_link_dict["subscription_period"], int)
         assert invite_link_dict["subscription_price"] == self.subscription_price
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, invite_link):
+        if PTB_TIMEDELTA:
+            assert invite_link.subscription_period == self.subscription_period
+            assert isinstance(invite_link.subscription_period, dtm.timedelta)
+        else:
+            assert invite_link.subscription_period == int(self.subscription_period.total_seconds())
+            assert isinstance(invite_link.subscription_period, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, invite_link):
+        invite_link.subscription_period
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`subscription_period` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = ChatInviteLink("link", User(1, "", False), True, True, True)
         b = ChatInviteLink("link", User(1, "", False), True, True, True)
diff --git a/tests/test_error.py b/tests/test_error.py
index 9fd0ba707fc..863ec0c4c5e 100644
--- a/tests/test_error.py
+++ b/tests/test_error.py
@@ -16,6 +16,7 @@
 #
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
+import datetime as dtm
 import pickle
 from collections import defaultdict
 
@@ -35,6 +36,7 @@
     TimedOut,
 )
 from telegram.ext import InvalidCallbackData
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -92,9 +94,28 @@ def test_chat_migrated(self):
             raise ChatMigrated(1234)
         assert e.value.new_chat_id == 1234
 
-    def test_retry_after(self):
-        with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12 seconds"):
-            raise RetryAfter(12)
+    @pytest.mark.parametrize("retry_after", [12, dtm.timedelta(seconds=12)])
+    def test_retry_after(self, PTB_TIMEDELTA, retry_after):
+        if PTB_TIMEDELTA:
+            with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 0:00:12"):
+                raise (exception := RetryAfter(retry_after))
+            assert type(exception.retry_after) is dtm.timedelta
+        else:
+            with pytest.raises(RetryAfter, match="Flood control exceeded. Retry in 12 seconds"):
+                raise (exception := RetryAfter(retry_after))
+            assert type(exception.retry_after) is int
+
+    def test_retry_after_int_deprecated(self, PTB_TIMEDELTA, recwarn):
+        retry_after = RetryAfter(12).retry_after
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+            assert type(retry_after) is dtm.timedelta
+        else:
+            assert len(recwarn) == 1
+            assert "`retry_after` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+            assert type(retry_after) is int
 
     def test_conflict(self):
         with pytest.raises(Conflict, match="Something something."):
@@ -111,6 +132,7 @@ def test_conflict(self):
             (TimedOut(), ["message"]),
             (ChatMigrated(1234), ["message", "new_chat_id"]),
             (RetryAfter(12), ["message", "retry_after"]),
+            (RetryAfter(dtm.timedelta(seconds=12)), ["message", "retry_after"]),
             (Conflict("test message"), ["message"]),
             (PassportDecryptionError("test message"), ["message"]),
             (InvalidCallbackData("test data"), ["callback_data"]),
@@ -136,7 +158,7 @@ def test_errors_pickling(self, exception, attributes):
             (BadRequest("test message")),
             (TimedOut()),
             (ChatMigrated(1234)),
-            (RetryAfter(12)),
+            (RetryAfter(dtm.timedelta(seconds=12))),
             (Conflict("test message")),
             (PassportDecryptionError("test message")),
             (InvalidCallbackData("test data")),
@@ -181,15 +203,19 @@ def make_assertion(cls):
 
         make_assertion(TelegramError)
 
-    def test_string_representations(self):
+    def test_string_representations(self, PTB_TIMEDELTA):
         """We just randomly test a few of the subclasses - should suffice"""
         e = TelegramError("This is a message")
         assert repr(e) == "TelegramError('This is a message')"
         assert str(e) == "This is a message"
 
-        e = RetryAfter(42)
-        assert repr(e) == "RetryAfter('Flood control exceeded. Retry in 42 seconds')"
-        assert str(e) == "Flood control exceeded. Retry in 42 seconds"
+        e = RetryAfter(dtm.timedelta(seconds=42))
+        if PTB_TIMEDELTA:
+            assert repr(e) == "RetryAfter('Flood control exceeded. Retry in 0:00:42')"
+            assert str(e) == "Flood control exceeded. Retry in 0:00:42"
+        else:
+            assert repr(e) == "RetryAfter('Flood control exceeded. Retry in 42 seconds')"
+            assert str(e) == "Flood control exceeded. Retry in 42 seconds"
 
         e = BadRequest("This is a message")
         assert repr(e) == "BadRequest('This is a message')"
diff --git a/tests/test_messageautodeletetimerchanged.py b/tests/test_messageautodeletetimerchanged.py
index 19133e9aaa9..9e0ab16476f 100644
--- a/tests/test_messageautodeletetimerchanged.py
+++ b/tests/test_messageautodeletetimerchanged.py
@@ -16,12 +16,15 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 
+import datetime as dtm
+
 from telegram import MessageAutoDeleteTimerChanged, VideoChatEnded
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
 class TestMessageAutoDeleteTimerChangedWithoutRequest:
-    message_auto_delete_time = 100
+    message_auto_delete_time = dtm.timedelta(seconds=100)
 
     def test_slot_behaviour(self):
         action = MessageAutoDeleteTimerChanged(self.message_auto_delete_time)
@@ -30,18 +33,47 @@ def test_slot_behaviour(self):
         assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
 
     def test_de_json(self):
-        json_dict = {"message_auto_delete_time": self.message_auto_delete_time}
+        json_dict = {
+            "message_auto_delete_time": int(self.message_auto_delete_time.total_seconds())
+        }
         madtc = MessageAutoDeleteTimerChanged.de_json(json_dict, None)
         assert madtc.api_kwargs == {}
 
-        assert madtc.message_auto_delete_time == self.message_auto_delete_time
+        assert madtc._message_auto_delete_time == self.message_auto_delete_time
 
     def test_to_dict(self):
         madtc = MessageAutoDeleteTimerChanged(self.message_auto_delete_time)
         madtc_dict = madtc.to_dict()
 
         assert isinstance(madtc_dict, dict)
-        assert madtc_dict["message_auto_delete_time"] == self.message_auto_delete_time
+        assert madtc_dict["message_auto_delete_time"] == int(
+            self.message_auto_delete_time.total_seconds()
+        )
+        assert isinstance(madtc_dict["message_auto_delete_time"], int)
+
+    def test_time_period_properties(self, PTB_TIMEDELTA):
+        message_auto_delete_time = MessageAutoDeleteTimerChanged(
+            self.message_auto_delete_time
+        ).message_auto_delete_time
+
+        if PTB_TIMEDELTA:
+            assert message_auto_delete_time == self.message_auto_delete_time
+            assert isinstance(message_auto_delete_time, dtm.timedelta)
+        else:
+            assert message_auto_delete_time == int(self.message_auto_delete_time.total_seconds())
+            assert isinstance(message_auto_delete_time, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA):
+        MessageAutoDeleteTimerChanged(self.message_auto_delete_time).message_auto_delete_time
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`message_auto_delete_time` will be of type `datetime.timedelta`" in str(
+                recwarn[0].message
+            )
+            assert recwarn[0].category is PTBDeprecationWarning
 
     def test_equality(self):
         a = MessageAutoDeleteTimerChanged(100)
diff --git a/tests/test_official/arg_type_checker.py b/tests/test_official/arg_type_checker.py
index 4b0e3630691..f5e7cb414e5 100644
--- a/tests/test_official/arg_type_checker.py
+++ b/tests/test_official/arg_type_checker.py
@@ -68,7 +68,7 @@
     """,
     re.VERBOSE,
 )
-TIMEDELTA_REGEX = re.compile(r"\w+_period$")  # Parameter names ending with "_period"
+TIMEDELTA_REGEX = re.compile(r"(in|number of) seconds")
 
 log = logging.debug
 
@@ -194,15 +194,9 @@ def check_param_type(
         mapped_type = dtm.datetime if is_class else mapped_type | dtm.datetime
 
     # 4) HANDLING TIMEDELTA:
-    elif re.search(TIMEDELTA_REGEX, ptb_param.name) and obj.__name__ in (
-        "TransactionPartnerUser",
-        "create_invoice_link",
-    ):
-        # Currently we only support timedelta for `subscription_period` in `TransactionPartnerUser`
-        # and `create_invoice_link`.
-        # See https://github.com/python-telegram-bot/python-telegram-bot/issues/4575
+    elif re.search(TIMEDELTA_REGEX, tg_parameter.param_description):
         log("Checking that `%s` is a timedelta!\n", ptb_param.name)
-        mapped_type = dtm.timedelta if is_class else mapped_type | dtm.timedelta
+        mapped_type = mapped_type | dtm.timedelta
 
     # 5) COMPLEX TYPES:
     # Some types are too complicated, so we replace our annotation with a simpler type:
diff --git a/tests/test_official/exceptions.py b/tests/test_official/exceptions.py
index 40144f803d3..d86819d649b 100644
--- a/tests/test_official/exceptions.py
+++ b/tests/test_official/exceptions.py
@@ -17,7 +17,6 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program.  If not, see [http://www.gnu.org/licenses/].
 """This module contains exceptions to our API compared to the official API."""
-import datetime as dtm
 
 from telegram import Animation, Audio, Document, Gift, PhotoSize, Sticker, Video, VideoNote, Voice
 from tests.test_official.helpers import _get_params_base
@@ -55,12 +54,6 @@ class ParamTypeCheckingExceptions:
         "replace_sticker_in_set": {
             "old_sticker$": Sticker,
         },
-        # The underscore will match any method
-        r"\w+_[\w_]+": {
-            "duration": dtm.timedelta,
-            r"\w+_period": dtm.timedelta,
-            "cache_time": dtm.timedelta,
-        },
     }
 
     # TODO: Look into merging this with COMPLEX_TYPES
@@ -102,7 +95,6 @@ class ParamTypeCheckingExceptions:
         },
         "InputProfilePhotoAnimated": {
             "animation": str,  # actual: Union[str, FileInput]
-            "main_frame_timestamp": float,  # actual: Union[float, dtm.timedelta]
         },
         "InputSticker": {
             "sticker": str,  # actual: Union[str, FileInput]
@@ -110,8 +102,9 @@ class ParamTypeCheckingExceptions:
         "InputStoryContent.*": {
             "photo": str,  # actual: Union[str, FileInput]
             "video": str,  # actual: Union[str, FileInput]
-            "duration": float,  # actual: dtm.timedelta
-            "cover_frame_timestamp": float,  # actual: dtm.timedelta
+        },
+        "TransactionPartnerUser": {
+            "subscription_period": int,  # actual: Union[int, dtm.timedelta]
         },
         "EncryptedPassportElement": {
             "data": str,  # actual: Union[IdDocumentData, PersonalDetails, ResidentialAddress]
diff --git a/tests/test_paidmedia.py b/tests/test_paidmedia.py
index a696c416b58..8055e161e84 100644
--- a/tests/test_paidmedia.py
+++ b/tests/test_paidmedia.py
@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU Lesser Public License
 # along with this program. If not, see [http://www.gnu.org/licenses/].
 
+import datetime as dtm
 from copy import deepcopy
 
 import pytest
@@ -34,6 +35,7 @@
     Video,
 )
 from telegram.constants import PaidMediaType
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -46,13 +48,13 @@ class PaidMediaTestBase:
     type = PaidMediaType.PHOTO
     width = 640
     height = 480
-    duration = 60
+    duration = dtm.timedelta(60)
     video = Video(
         file_id="video_file_id",
         width=640,
         height=480,
         file_unique_id="file_unique_id",
-        duration=60,
+        duration=dtm.timedelta(seconds=60),
     )
     photo = (
         PhotoSize(
@@ -96,14 +98,17 @@ def test_de_json_subclass(self, offline_bot, pm_type, subclass):
             "photo": [p.to_dict() for p in self.photo],
             "width": self.width,
             "height": self.height,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
         }
         pm = PaidMedia.de_json(json_dict, offline_bot)
 
+        # TODO: Should be removed when the timedelta migartion is complete
+        extra_slots = {"duration"} if subclass is PaidMediaPreview else set()
+
         assert type(pm) is subclass
-        assert set(pm.api_kwargs.keys()) == set(json_dict.keys()) - set(subclass.__slots__) - {
-            "type"
-        }
+        assert set(pm.api_kwargs.keys()) == set(json_dict.keys()) - (
+            set(subclass.__slots__) | extra_slots
+        ) - {"type"}
         assert pm.type == pm_type
 
     def test_to_dict(self, paid_media):
@@ -243,21 +248,23 @@ def test_de_json(self, offline_bot):
         json_dict = {
             "width": self.width,
             "height": self.height,
-            "duration": self.duration,
+            "duration": int(self.duration.total_seconds()),
         }
         pmp = PaidMediaPreview.de_json(json_dict, offline_bot)
         assert pmp.width == self.width
         assert pmp.height == self.height
-        assert pmp.duration == self.duration
+        assert pmp._duration == self.duration
         assert pmp.api_kwargs == {}
 
     def test_to_dict(self, paid_media_preview):
-        assert paid_media_preview.to_dict() == {
-            "type": paid_media_preview.type,
-            "width": self.width,
-            "height": self.height,
-            "duration": self.duration,
-        }
+        paid_media_preview_dict = paid_media_preview.to_dict()
+
+        assert isinstance(paid_media_preview_dict, dict)
+        assert paid_media_preview_dict["type"] == paid_media_preview.type
+        assert paid_media_preview_dict["width"] == paid_media_preview.width
+        assert paid_media_preview_dict["height"] == paid_media_preview.height
+        assert paid_media_preview_dict["duration"] == int(self.duration.total_seconds())
+        assert isinstance(paid_media_preview_dict["duration"], int)
 
     def test_equality(self, paid_media_preview):
         a = paid_media_preview
@@ -266,6 +273,11 @@ def test_equality(self, paid_media_preview):
             height=self.height,
             duration=self.duration,
         )
+        x = PaidMediaPreview(
+            width=self.width,
+            height=self.height,
+            duration=int(self.duration.total_seconds()),
+        )
         c = PaidMediaPreview(
             width=100,
             height=100,
@@ -274,7 +286,9 @@ def test_equality(self, paid_media_preview):
         d = Dice(5, "test")
 
         assert a == b
+        assert b == x
         assert hash(a) == hash(b)
+        assert hash(b) == hash(x)
 
         assert a != c
         assert hash(a) != hash(c)
@@ -282,6 +296,26 @@ def test_equality(self, paid_media_preview):
         assert a != d
         assert hash(a) != hash(d)
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, paid_media_preview):
+        duration = paid_media_preview.duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, paid_media_preview):
+        paid_media_preview.duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
 
 # ===========================================================================================
 # ===========================================================================================
diff --git a/tests/test_poll.py b/tests/test_poll.py
index c7e3da447f5..484e18710a2 100644
--- a/tests/test_poll.py
+++ b/tests/test_poll.py
@@ -22,6 +22,7 @@
 from telegram import Chat, InputPollOption, MessageEntity, Poll, PollAnswer, PollOption, User
 from telegram._utils.datetime import UTC, to_timestamp
 from telegram.constants import PollType
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -295,7 +296,7 @@ class PollTestBase:
         b"\\u200d\\U0001f467\\U0001f431http://google.com"
     ).decode("unicode-escape")
     explanation_entities = [MessageEntity(13, 17, MessageEntity.URL)]
-    open_period = 42
+    open_period = dtm.timedelta(seconds=42)
     close_date = dtm.datetime.now(dtm.timezone.utc)
     question_entities = [
         MessageEntity(MessageEntity.BOLD, 0, 4),
@@ -316,7 +317,7 @@ def test_de_json(self, offline_bot):
             "allows_multiple_answers": self.allows_multiple_answers,
             "explanation": self.explanation,
             "explanation_entities": [self.explanation_entities[0].to_dict()],
-            "open_period": self.open_period,
+            "open_period": int(self.open_period.total_seconds()),
             "close_date": to_timestamp(self.close_date),
             "question_entities": [e.to_dict() for e in self.question_entities],
         }
@@ -337,7 +338,7 @@ def test_de_json(self, offline_bot):
         assert poll.allows_multiple_answers == self.allows_multiple_answers
         assert poll.explanation == self.explanation
         assert poll.explanation_entities == tuple(self.explanation_entities)
-        assert poll.open_period == self.open_period
+        assert poll._open_period == self.open_period
         assert abs(poll.close_date - self.close_date) < dtm.timedelta(seconds=1)
         assert to_timestamp(poll.close_date) == to_timestamp(self.close_date)
         assert poll.question_entities == tuple(self.question_entities)
@@ -354,7 +355,7 @@ def test_de_json_localization(self, tz_bot, offline_bot, raw_bot):
             "allows_multiple_answers": self.allows_multiple_answers,
             "explanation": self.explanation,
             "explanation_entities": [self.explanation_entities[0].to_dict()],
-            "open_period": self.open_period,
+            "open_period": int(self.open_period.total_seconds()),
             "close_date": to_timestamp(self.close_date),
             "question_entities": [e.to_dict() for e in self.question_entities],
         }
@@ -387,10 +388,28 @@ def test_to_dict(self, poll):
         assert poll_dict["allows_multiple_answers"] == poll.allows_multiple_answers
         assert poll_dict["explanation"] == poll.explanation
         assert poll_dict["explanation_entities"] == [poll.explanation_entities[0].to_dict()]
-        assert poll_dict["open_period"] == poll.open_period
+        assert poll_dict["open_period"] == int(self.open_period.total_seconds())
         assert poll_dict["close_date"] == to_timestamp(poll.close_date)
         assert poll_dict["question_entities"] == [e.to_dict() for e in poll.question_entities]
 
+    def test_time_period_properties(self, PTB_TIMEDELTA, poll):
+        if PTB_TIMEDELTA:
+            assert poll.open_period == self.open_period
+            assert isinstance(poll.open_period, dtm.timedelta)
+        else:
+            assert poll.open_period == int(self.open_period.total_seconds())
+            assert isinstance(poll.open_period, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA, poll):
+        poll.open_period
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`open_period` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
+
     def test_equality(self):
         a = Poll(123, "question", ["O1", "O2"], 1, False, True, Poll.REGULAR, True)
         b = Poll(123, "question", ["o1", "o2"], 1, True, False, Poll.REGULAR, True)
diff --git a/tests/test_videochat.py b/tests/test_videochat.py
index 57d91003c29..df8151940cf 100644
--- a/tests/test_videochat.py
+++ b/tests/test_videochat.py
@@ -28,6 +28,7 @@
     VideoChatStarted,
 )
 from telegram._utils.datetime import UTC, to_timestamp
+from telegram.warnings import PTBDeprecationWarning
 from tests.auxil.slots import mro_slots
 
 
@@ -60,7 +61,7 @@ def test_to_dict(self):
 
 
 class TestVideoChatEndedWithoutRequest:
-    duration = 100
+    duration = dtm.timedelta(seconds=100)
 
     def test_slot_behaviour(self):
         action = VideoChatEnded(8)
@@ -69,27 +70,50 @@ def test_slot_behaviour(self):
         assert len(mro_slots(action)) == len(set(mro_slots(action))), "duplicate slot"
 
     def test_de_json(self):
-        json_dict = {"duration": self.duration}
+        json_dict = {"duration": int(self.duration.total_seconds())}
         video_chat_ended = VideoChatEnded.de_json(json_dict, None)
         assert video_chat_ended.api_kwargs == {}
 
-        assert video_chat_ended.duration == self.duration
+        assert video_chat_ended._duration == self.duration
 
     def test_to_dict(self):
         video_chat_ended = VideoChatEnded(self.duration)
         video_chat_dict = video_chat_ended.to_dict()
 
         assert isinstance(video_chat_dict, dict)
-        assert video_chat_dict["duration"] == self.duration
+        assert video_chat_dict["duration"] == int(self.duration.total_seconds())
+
+    def test_time_period_properties(self, PTB_TIMEDELTA):
+        duration = VideoChatEnded(duration=self.duration).duration
+
+        if PTB_TIMEDELTA:
+            assert duration == self.duration
+            assert isinstance(duration, dtm.timedelta)
+        else:
+            assert duration == int(self.duration.total_seconds())
+            assert isinstance(duration, int)
+
+    def test_time_period_int_deprecated(self, recwarn, PTB_TIMEDELTA):
+        VideoChatEnded(self.duration).duration
+
+        if PTB_TIMEDELTA:
+            assert len(recwarn) == 0
+        else:
+            assert len(recwarn) == 1
+            assert "`duration` will be of type `datetime.timedelta`" in str(recwarn[0].message)
+            assert recwarn[0].category is PTBDeprecationWarning
 
     def test_equality(self):
         a = VideoChatEnded(100)
         b = VideoChatEnded(100)
+        x = VideoChatEnded(dtm.timedelta(seconds=100))
         c = VideoChatEnded(50)
         d = VideoChatStarted()
 
         assert a == b
         assert hash(a) == hash(b)
+        assert b == x
+        assert hash(b) == hash(x)
 
         assert a != c
         assert hash(a) != hash(c)


Code review comments:
diff_hunk: @@ -0,0 +1,36 @@
+features = "Use `timedelta` to represent time periods in classes"
path: changes/unreleased/4750.jJBu7iAgZa96hdqcpHK96W.toml
text: @user1:
```suggestion
features = "Use :class:`datetime.timedelta` to represent time periods in class arguments and attributes"
```

diff_hunk: @@ -665,3 +680,30 @@ def set_bot(self, bot: Optional["Bot"]) -> None:
             bot (:class:`telegram.Bot` | :obj:`None`): The bot instance.
         """
         self._bot = bot
+
+    # We use str keys to avoid importing which causes circular dependencies
+    _TIME_PERIOD_DEPRECATIONS: ClassVar = {
path: src/telegram/_telegramobject.py
text: @user1:
This could just be a module level constant, no need to increase memory by adding this to every object imo

@author:
> increase memory by adding this to every object 

would it tho 🤔. 
shouldn't it being a static class variable mean that all instances of this class and subclasses will share the same memory location of this list.

will update in case i'm missing something.

@author:
I did a bit of research on python's class variables, and as I was able to gather it would become a problem but only if one of the subclasses reassigned the `_TIME_PERIOD_DEPRECATIONS`. We are not doing that, and so shouldn't users since it's a private variable.

could you point out how it would affect memory from your pov 😅

@user1:
ah well after some testing of my own too, It seems that python does use the reference of the object across many instances, so the memory isn't added to every object. So even a module level constant would mean that it takes the same memory overall in the end.

I guess the only reason now to move it to a module constant is that it's something only used internally and not relevant to a TelegramObject at all.

@author:
>  it's something only used internally and not relevant to a TelegramObject at all.

Fair point right there 👍

diff_hunk: @@ -224,3 +228,47 @@ def _datetime_to_float_timestamp(dt_obj: dtm.datetime) -> float:
     if dt_obj.tzinfo is None:
         dt_obj = dt_obj.replace(tzinfo=dtm.timezone.utc)
     return dt_obj.timestamp()
+
+
+def get_timedelta_value(
+    value: Optional[dtm.timedelta], attribute: str
+) -> Optional[Union[int, dtm.timedelta]]:
+    """
+    Convert a `datetime.timedelta` to seconds or return it as-is, based on environment config.
+
+    This utility is part of the migration process from integer-based time representations
+    to using `datetime.timedelta`. The behavior is controlled by the `PTB_TIMEDELTA`
+    environment variable.
+
+    Note:
+        When `PTB_TIMEDELTA` is not enabled, the function will issue a deprecation warning.
+
+    Args:
+        value (:obj:`datetime.timedelta`): The timedelta value to process.
+        attribute (:obj:`str`): The name of the attribute at the caller scope, used for
+            warning messages.
+
+    Returns:
+        - :obj:`None` if :paramref:`value` is None.
+        - :obj:`datetime.timedelta` if `PTB_TIMEDELTA=true`.
+        - :obj:`int` if the total seconds is a whole number.
+        - float: otherwise.
+    """
+    if value is None:
+        return None
+    if os.getenv("PTB_TIMEDELTA", "false").lower().strip() == "true":
path: src/telegram/_utils/datetime.py
text: @user1:
we should check for PTB_TIMEDELTA=true _and_ PTB_TIMEDELTA=1. Question is whether general truthiness would count, i.e. `PTB_TIMEDELTA=something_not_false_or_zero`? I don't think it should.

@user2:
"explicit is better than implicit" :clown: allowing `1` would be okay for me, but I'd be perfectly fine with restricting to `true` (case insensitive)

@author:
don't mind adding 1. currently I've set it up to mimic how `env_var_2_bool` works.. but thats for internal use up till now

diff_hunk: @@ -499,6 +499,13 @@ def _apply_api_kwargs(self, api_kwargs: JSONDict) -> None:
             elif getattr(self, key, True) is None:
                 setattr(self, key, api_kwargs.pop(key))
 
+    def _is_deprecated_attr(self, attr: str) -> bool:
+        """Checks wheather `attr` is in the list of deprecated time period attributes."""
path: src/telegram/_telegramobject.py
text: @user1:
```suggestion
        """Checks whether `attr` is in the list of deprecated time period attributes."""
```

diff_hunk: @@ -558,7 +569,7 @@ def _get_attrs(
                 if recursive and hasattr(value, "to_dict"):
                     data[key] = value.to_dict(recursive=True)
                 else:
-                    data[key] = value
+                    data[key.removeprefix("_") if self._is_deprecated_attr(key) else key] = value
path: src/telegram/_telegramobject.py
text: @user1:
why are we then removing the _ after including it intentionally in _get_attrs_names? This would mean we would not get private attributes which are deprecated when include_private=True.

If the intention is to return only the public facing attribute of something like Video.duration (because we don't want the internal Video._duration), then the logic should be in line 644, to change the key name (see my next comment).

@user2:
that's a fair point 👍 

@author:
> why are we then removing the _ after including it intentionally in _get_attrs_names? 

the underscore is included in `_TIME_PERIOD_DEPRECATIONS` to make extraction of the timedelta value latter a bit easier. and as you noted it's removed not to submit to TG invalid keys.

> This would mean we would not get private attributes which are deprecated when include_private=True.

I don't fully understand this part. Currently private attributes that are not on the deprecation list (e.g. `_freeze`, `_id_attrs`) will function as normal when if include_private=True. Couldn't understand what you mean by "private attributes which are deprecated"

diff_hunk: @@ -629,7 +640,11 @@ def to_dict(self, recursive: bool = True) -> JSONDict:
             elif isinstance(value, dtm.datetime):
                 out[key] = to_timestamp(value)
             elif isinstance(value, dtm.timedelta):
-                out[key] = value.total_seconds()
+                # Converting to int here is neccassry in some cases where Bot API returns
+                # 'BadRquest' when expecting integers (e.g. Video.duration)
+                out[key] = (
path: src/telegram/_telegramobject.py
text: @user1:
```suggestion
                out[key.removeprefix('_')] = (
```
and then,`pop_keys.add(key)`. 

This would keep the behaviour of the other functions untouched.

@author:
If i'm understanding correctly this would only substitute the logic in L572 ` in_get_attrs()` 😅?

is the advantage of this that here we wouldn't need the if check (above comment) and not get in the way of unaffected TG objects?

if yes, note that this way is changing the dict size while iterating, since this is not a simple overwrite. can work around it if it's still better.
@user1 @user2-Joshi 


@user1:
> is the advantage of this that here we wouldn't need the if check (above comment) and not get in the way of unaffected TG objects?

yes indeed. I'm just trying to make sure that there is separation of concerns.

diff_hunk: @@ -62,28 +66,54 @@ class VideoChatEnded(TelegramObject):
     .. versionchanged:: 20.0
         This class was renamed from ``VoiceChatEnded`` in accordance to Bot API 6.0.
 
+    .. versionchanged:: NEXT.VERSION
+       As part of the migration to representing time periods using ``datetime.timedelta``,
+       equality comparison now considers integer durations and equivalent timedeltas as equal.
+
     Args:
-        duration (:obj:`int`): Voice chat duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Voice chat duration
+            in seconds.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
-        duration (:obj:`int`): Voice chat duration in seconds.
+        duration (:obj:`int` | :class:`datetime.timedelta`): Voice chat duration in seconds.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
 
     """
 
-    __slots__ = ("duration",)
+    __slots__ = ("_duration",)
 
     def __init__(
         self,
-        duration: int,
+        duration: TimePeriod,
         *,
         api_kwargs: Optional[JSONDict] = None,
     ) -> None:
         super().__init__(api_kwargs=api_kwargs)
-        self.duration: int = duration
-        self._id_attrs = (self.duration,)
+        self._duration: dtm.timedelta = parse_period_arg(duration)  # type: ignore[assignment]
+        self._id_attrs = (self._duration,)
 
         self._freeze()
 
+    @property
+    def duration(self) -> Union[int, dtm.timedelta]:
+        return get_timedelta_value(  # type: ignore[return-value]
+            self._duration, attribute="duration"
+        )
+
+    @classmethod
+    def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "VideoChatEnded":
+        """See :meth:`telegram.TelegramObject.de_json`."""
+        data = cls._parse_data(data)
+
+        data["duration"] = dtm.timedelta(seconds=s) if (s := data.get("duration")) else None
+
+        return super().de_json(data=data, bot=bot)
+
path: src/telegram/_videochat.py
text: @user1:
```suggestion
```
don't need this if it's done in the `__init__`? I guess this is the same as https://github.com/python-telegram-bot/python-telegram-bot/pull/4750#discussion_r2121930855

diff_hunk: @@ -208,21 +213,43 @@ class RetryAfter(TelegramError):
        :attr:`retry_after` is now an integer to comply with the Bot API.
 
     Args:
-        retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
+        retry_after (:obj:`int` | :class:`datetime.timedelta`): Time in seconds, after which the
+            bot can retry the request.
+
+            .. versionchanged:: NEXT.VERSION
+                |time-period-input|
 
     Attributes:
-        retry_after (:obj:`int`): Time in seconds, after which the bot can retry the request.
+        retry_after (:obj:`int` | :class:`datetime.timedelta`): Time in seconds, after which the
+            bot can retry the request.
+
+            .. deprecated:: NEXT.VERSION
+                |time-period-int-deprecated|
 
     """
 
-    __slots__ = ("retry_after",)
+    __slots__ = ("_retry_after",)
+
+    def __init__(self, retry_after: TimePeriod):
+        self._retry_after: dtm.timedelta = parse_period_arg(  # type: ignore[assignment]
path: src/telegram/error.py
text: @user1:
the type checker is right in this case

@user2:
you mean b/c the return value is `timedelta | None` instead of `timedelta`? If you want to improve type hinting for `parse_period_arg` for that, you can use `typing.overload` to say `None → None` and `int | float | td → td`

@author:
> the type checker is right in this case

How come 🤔. `retry_after` is a required argument, although optional in API docs but I've assumed that's why it's ignored in `test_official`

@user1:
yeah that would fix it.

diff_hunk: @@ -600,6 +631,13 @@ def de_json(cls, data: JSONDict, bot: Optional["Bot"] = None) -> "ChatFullInfo":
             Message,
         )
 
+        data["slow_mode_delay"] = (
+            dtm.timedelta(seconds=s) if (s := data.get("slow_mode_delay")) else None
path: src/telegram/_chatfullinfo.py
text: @user1:
```suggestion
            parse_period_arg(data.get("slow_mode_delay"))
```

Alternatively, I'd also be okay with not doing a conversion in de_json at all and instead letting that happen in `__init__`. This comment basically applies in all places 😅 Or am I maybe missing something why this should definitely happen here? :)

@author:
Oh right!
these were done here not cause a warning; early on when passing `int`s to `__init__`s was also deprecated.

diff_hunk: @@ -629,7 +640,11 @@ def to_dict(self, recursive: bool = True) -> JSONDict:
             elif isinstance(value, dtm.datetime):
                 out[key] = to_timestamp(value)
             elif isinstance(value, dtm.timedelta):
-                out[key] = value.total_seconds()
+                # Converting to int here is neccassry in some cases where Bot API returns
+                # 'BadRquest' when expecting integers (e.g. Video.duration)
+                out[key] = (
+                    int(seconds) if (seconds := value.total_seconds()).is_integer() else seconds
+                )
path: src/telegram/_telegramobject.py
text: @user1:
interesting catch! I'm wondering if there are cases were a float is accepted as time duration. that would be a hard-to-catch bug, if the user passes a float and the int-version is passed  by TG 🤔 

@author:
Oh I meant `InputMediaVideo` there, will update comment.

Ah yes unfortunately :( and to make matters worse they're Business related
`InputStoryContentVideo.[duration/cover_frame_timestamp]`
`InputProfilePhotoAnimated.main_frame_timestamp`

would it suffice if we tested whether telegram returns `ok` if provided integers instead of floats, plus a code comment?

this also reminded me one these classes is using it's own float -> timedelta parsing logic. 👍 

@user1:
ah, thanks for the info! Pointing out the currently known use cases should suffice IMO. If you want to, adding a special test case for *one* of them would be plenty - you could add a comment along the lines "this applies also to other classes e.g. …, but we test it only for one as this suffices to ensure that the logic works" :)

@author:
hmm we can't really test this in the test suite, because it requires a Telegram Business account. I can prepare a minimal example to send an actual request if a team member has access to a test business account. 
@user2 (or others) would someone be able help verify this?

For context I want to ensure that making a request like this:
```python
await def post_story(
    self,
    "business_connection_id",
    content=InputStoryContentVideo(
        video=video.read_bytes(),
        duration=10.0, # or datetime.timedelta(seconds=10)
        cover_frame_timestamp=5.0, # or datetime.timedelta(seconds=5)
        is_animation=False,
    ),
)
```
to telegram does not raise a `BadRequest` for invalid `duration` or `cover_frame_timestamp`. and a similar request for `bot.set_business_account_profile_photo` with `photo=InputProfilePhotoAnimated`.

I can run the example or provide it so that someone could :)

@user1:
mh, I don't have a business account and I fear that setting up something for them in the tests would be a hassle. Alternatively, you can mock the tg response by mocking `httpx.post` and returning an error if the payload contains a float 

@author:
Aha no problem!
I'm just thinking mocking the request would be of little use, because we don't know the actual Telegram behavior (throw, or not throw)
> mocking httpx.post and returning an error if the payload contains a float

Also in the case of two methods above, passing a float to Telegram should be successful, it's integers that might (or might not) throw BadRequest errors.

@user3:
I do have a business account, is there an example you'd like me to run?

@user1:
>Also in the case of two methods above, passing a float to Telegram should be successful, it's integers that might (or might not) throw BadRequest errors.

okay, the other way around :D 

>I'm just thinking mocking the request would be of little use, because we don't know the actual Telegram behavior (throw, or not throw)

That's true, it's not perfect. What we would achieve with this is that we avoid regressions in what we think is the right implementation :)

@author:
> okay, the other way around :D

haha thought i was the lost one for a sec xD

here is the test bot.
<details>
<summary>float_periods_bot.py</summary>

```python
#!/usr/bin/env python

import asyncio
import datetime
import logging

from telegram import Update
from telegram._files._inputstorycontent import InputStoryContentVideo
from telegram._files.inputprofilephoto import InputProfilePhotoAnimated
from telegram.constants import StoryLimit
from telegram.ext import Application, CommandHandler, ContextTypes
from tests.auxil.files import data_file

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logging.getLogger("httpx").setLevel(logging.DEBUG)

logger = logging.getLogger(__name__)


async def post_story(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    for duration, cover_timestamp in [
        (4.0, 2.0),
        (4, 2),
        (datetime.timedelta(seconds=4.0), datetime.timedelta(seconds=2.0)),
    ]:
        content = InputStoryContentVideo(
            video=data_file("telegram.mp4").read_bytes(),
            duration=duration,
            cover_frame_timestamp=cover_timestamp,
        )
        logging.info("Posting story with arguments of type %s", type(duration))
        logging.info("content: %s", content.to_dict())

        await context.bot.post_story(
            "BUSINESS_CONNECTION_ID",
            content=content,
            active_period=StoryLimit.ACTIVITY_SIX_HOURS,
        )
        await asyncio.sleep(0.5)


async def set_profile_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Send a message when the command /help is issued."""
    for timestamp in [
        1.0,
        1,
        datetime.timedelta(seconds=1),
    ]:
        photo = InputProfilePhotoAnimated(
            animation=data_file("telegram2.mp4").read_bytes(),
            main_frame_timestamp=1.0,
        )
        logging.info("Setting profile photo with arguments of type %s", type(timestamp))
        logging.info("photo %s", photo.to_dict())
        await context.bot.set_business_account_profile_photo(
            "BUSINESS_CONNECTION_ID",
            photo=photo,
            is_public=False,
        )
        await asyncio.sleep(0.5)


def main() -> None:
    """Start the bot."""
    application = Application.builder().token("TOKEN").build()

    application.add_handler(CommandHandler("post_story", post_story))
    application.add_handler(CommandHandler("set_profile_photo", set_profile_photo))

    application.run_polling(allowed_updates=Update.ALL_TYPES)


if __name__ == "__main__":
    main()
```
</details>

thank you so, very much!

@author:
as discussed internally here https://t.me/c/1494805131/45463, An actual request for `post_story ` ran successfully with  integers in place of floats.

diff_hunk: @@ -50,6 +51,22 @@ def parse_sequence_arg(arg: Optional[Sequence[T]]) -> tuple[T, ...]:
     return tuple(arg) if arg else ()
 
 
+def parse_period_arg(arg: Optional[Union[int, float, dtm.timedelta]]) -> Optional[dtm.timedelta]:
path: src/telegram/_utils/argumentparsing.py
text: @user1:
```suggestion
def to_timedelta(arg: Optional[Union[int, float, dtm.timedelta]]) -> Optional[dtm.timedelta]:
```
would be a more apt name