Repository: pandas-dev/pandas

Issue:
BUG: numpy.timedelta64[M] not properly raising in pandas.Timedelta in 2.0.0
### Pandas version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the [latest version](https://pandas.pydata.org/docs/whatsnew/index.html) of pandas.

- [ ] I have confirmed this bug exists on the [main branch](https://pandas.pydata.org/docs/dev/getting_started/install.html#installing-the-development-version-of-pandas) of pandas.


### Reproducible Example

```python
import pandas as pd
import numpy as np

In [1]: pd.Timedelta(np.timedelta64(1, "M"))
Out[1]: Timedelta('31 days 00:00:00')

In [2]: np.timedelta64(1, "M").astype('timedelta64[D]')
Out[2]: numpy.timedelta64(30,'D')
```


### Issue Description

A numpy `numpy.timedelta64(1,'M')` is converted by `pandas.Timedelta` as an interval of 31 days.

### Expected Behavior

It should be converted to an interval of 30 days.

In pandas 1.5.x it was converted to `Timedelta('30 days 10:29:06')`

### Installed Versions

<details>

INSTALLED VERSIONS
------------------
commit           : 478d340667831908b5b4bf09a2787a11a14560c9
python           : 3.11.1.final.0
python-bits      : 64
OS               : Darwin
OS-release       : 22.4.0
Version          : Darwin Kernel Version 22.4.0: Mon Mar  6 20:59:28 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T6000
machine          : arm64
processor        : arm
byteorder        : little
LC_ALL           : None
LANG             : None
LOCALE           : None.UTF-8

pandas           : 2.0.0
numpy            : 1.24.2
pytz             : 2023.3
dateutil         : 2.8.2
setuptools       : 65.5.0
pip              : 22.3.1
Cython           : None
pytest           : None
hypothesis       : None
sphinx           : None
blosc            : None
feather          : None
xlsxwriter       : None
lxml.etree       : None
html5lib         : None
pymysql          : None
psycopg2         : None
jinja2           : None
IPython          : 8.12.0
pandas_datareader: None
bs4              : None
bottleneck       : None
brotli           : None
fastparquet      : None
fsspec           : None
gcsfs            : None
matplotlib       : None
numba            : None
numexpr          : None
odfpy            : None
openpyxl         : None
pandas_gbq       : None
pyarrow          : None
pyreadstat       : None
pyxlsb           : None
s3fs             : None
scipy            : None
snappy           : None
sqlalchemy       : None
tables           : None
tabulate         : None
xarray           : None
xlrd             : None
zstandard        : None
tzdata           : 2023.3
qtpy             : None
pyqt5            : None
</details>



Discussion:
`In [1]:` should probably raise a ValueError. The equivalent `pd.Timedelta(1, unit="M")` now raises a ValueError

 `.astype('timedelta64[D]')` has a default `casting='unsafe'`, the equivalent `np.timedelta64(np.timedelta64(1, "M"), "D")` would raise 

```
In [3]: np.timedelta64(np.timedelta64(1, "M"), "D")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 np.timedelta64(np.timedelta64(1, "M"), "D")

TypeError: Cannot cast NumPy timedelta64 scalar from metadata [M] to [D] according to the rule 'same_kind'
```
Agreed with @mroeschke.

Side-note: I've been wondering recently if we should just map low-reso datetime64/timedelta64s to Period/DateOffsets.  It's not great typing-wise, but seems right abstraction-wise.
Going to jump on this
take

Patch:
diff --git a/doc/source/user_guide/timedeltas.rst b/doc/source/user_guide/timedeltas.rst
index 3a75aa0b39b1f..a6eb96f91a4bf 100644
--- a/doc/source/user_guide/timedeltas.rst
+++ b/doc/source/user_guide/timedeltas.rst
@@ -259,9 +259,6 @@ an alternative is to divide by another timedelta object. Note that division by t
    # to days
    td / np.timedelta64(1, "D")
 
-   # to months (these are constant months)
-   td / np.timedelta64(1, "M")
-
 Dividing or multiplying a ``timedelta64[ns]`` Series by an integer or integer Series
 yields another ``timedelta64[ns]`` dtypes Series.
 
diff --git a/doc/source/whatsnew/v2.1.0.rst b/doc/source/whatsnew/v2.1.0.rst
index 877ff32107971..447a9e05a1c3a 100644
--- a/doc/source/whatsnew/v2.1.0.rst
+++ b/doc/source/whatsnew/v2.1.0.rst
@@ -365,6 +365,7 @@ Timedelta
 ^^^^^^^^^
 - :meth:`TimedeltaIndex.map` with ``na_action="ignore"`` now works as expected (:issue:`51644`)
 - Bug in :class:`TimedeltaIndex` division or multiplication leading to ``.freq`` of "0 Days" instead of ``None`` (:issue:`51575`)
+- Bug in :class:`Timedelta` with Numpy timedelta64 objects not properly raising ``ValueError`` (:issue:`52806`)
 - Bug in :meth:`Timedelta.round` with values close to the implementation bounds returning incorrect results instead of raising ``OutOfBoundsTimedelta`` (:issue:`51494`)
 - Bug in :meth:`arrays.TimedeltaArray.map` and :meth:`TimedeltaIndex.map`, where the supplied callable operated array-wise instead of element-wise (:issue:`51977`)
 -
diff --git a/pandas/_libs/tslibs/dtypes.pxd b/pandas/_libs/tslibs/dtypes.pxd
index 1dca498f61b0e..a8dd88c763c14 100644
--- a/pandas/_libs/tslibs/dtypes.pxd
+++ b/pandas/_libs/tslibs/dtypes.pxd
@@ -9,6 +9,7 @@ cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) noexcept nogil
 cpdef int64_t periods_per_day(NPY_DATETIMEUNIT reso=*) except? -1
 cpdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1
 cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso)
+cpdef bint is_supported_unit(NPY_DATETIMEUNIT reso)
 
 cdef dict attrname_to_abbrevs
 cdef dict npy_unit_to_attrname
diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx
index 2a49ea6760e72..19f4c83e6cecf 100644
--- a/pandas/_libs/tslibs/dtypes.pyx
+++ b/pandas/_libs/tslibs/dtypes.pyx
@@ -321,7 +321,7 @@ cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso):
     return reso
 
 
-def is_supported_unit(NPY_DATETIMEUNIT reso):
+cpdef bint is_supported_unit(NPY_DATETIMEUNIT reso):
     return (
         reso == NPY_DATETIMEUNIT.NPY_FR_ns
         or reso == NPY_DATETIMEUNIT.NPY_FR_us
diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx
index e68b8b210437a..7319eccf3b51f 100644
--- a/pandas/_libs/tslibs/timedeltas.pyx
+++ b/pandas/_libs/tslibs/timedeltas.pyx
@@ -42,6 +42,7 @@ from pandas._libs.tslibs.conversion cimport (
 )
 from pandas._libs.tslibs.dtypes cimport (
     get_supported_reso,
+    is_supported_unit,
     npy_unit_to_abbrev,
 )
 from pandas._libs.tslibs.nattype cimport (
@@ -151,10 +152,10 @@ cdef dict timedelta_abbrevs = {
 
 _no_input = object()
 
-
 # ----------------------------------------------------------------------
 # API
 
+
 @cython.boundscheck(False)
 @cython.wraparound(False)
 def ints_to_pytimedelta(ndarray m8values, box=False):
@@ -1789,7 +1790,6 @@ class Timedelta(_Timedelta):
                 + int(kwargs.get("milliseconds", 0) * 1_000_000)
                 + seconds
             )
-
         if unit in {"Y", "y", "M"}:
             raise ValueError(
                 "Units 'M', 'Y', and 'y' are no longer supported, as they do not "
@@ -1836,6 +1836,15 @@ class Timedelta(_Timedelta):
                 return NaT
 
             reso = get_datetime64_unit(value)
+            if not (is_supported_unit(reso) or
+                    reso in [NPY_DATETIMEUNIT.NPY_FR_m,
+                             NPY_DATETIMEUNIT.NPY_FR_h,
+                             NPY_DATETIMEUNIT.NPY_FR_D,
+                             NPY_DATETIMEUNIT.NPY_FR_W,
+                             NPY_DATETIMEUNIT.NPY_FR_GENERIC]):
+                err = npy_unit_to_abbrev(reso)
+                raise ValueError(f" cannot construct a Timedelta from a unit {err}")
+
             new_reso = get_supported_reso(reso)
             if reso != NPY_DATETIMEUNIT.NPY_FR_GENERIC:
                 try:


Test patch:
diff --git a/pandas/tests/libs/test_lib.py b/pandas/tests/libs/test_lib.py
index 6ad8d748d6997..8583d8bcc052c 100644
--- a/pandas/tests/libs/test_lib.py
+++ b/pandas/tests/libs/test_lib.py
@@ -59,7 +59,7 @@ def test_fast_multiget_timedelta_resos(self):
         tm.assert_numpy_array_equal(result, expected)
 
         # case that can't be cast to td64ns
-        td = Timedelta(np.timedelta64(400, "Y"))
+        td = Timedelta(np.timedelta64(146000, "D"))
         assert hash(td) == hash(td.as_unit("ms"))
         assert hash(td) == hash(td.as_unit("us"))
         mapping1 = {td: 1}
diff --git a/pandas/tests/scalar/timedelta/test_constructors.py b/pandas/tests/scalar/timedelta/test_constructors.py
index ad9dd408fbeaf..7bd9e5fc5e293 100644
--- a/pandas/tests/scalar/timedelta/test_constructors.py
+++ b/pandas/tests/scalar/timedelta/test_constructors.py
@@ -289,7 +289,6 @@ def test_overflow_on_construction():
 @pytest.mark.parametrize(
     "val, unit",
     [
-        (3508, "M"),
         (15251, "W"),  # 1
         (106752, "D"),  # change from previous:
         (2562048, "h"),  # 0 hours
@@ -333,7 +332,6 @@ def test_construction_out_of_bounds_td64ns(val, unit):
 @pytest.mark.parametrize(
     "val, unit",
     [
-        (3508 * 10**9, "M"),
         (15251 * 10**9, "W"),
         (106752 * 10**9, "D"),
         (2562048 * 10**9, "h"),
diff --git a/pandas/tests/tslibs/test_timedeltas.py b/pandas/tests/tslibs/test_timedeltas.py
index 36ca02d32dbbd..2308aa27b60ab 100644
--- a/pandas/tests/tslibs/test_timedeltas.py
+++ b/pandas/tests/tslibs/test_timedeltas.py
@@ -72,6 +72,15 @@ def test_delta_to_nanoseconds_td64_MY_raises():
         delta_to_nanoseconds(td)
 
 
+@pytest.mark.parametrize("unit", ["Y", "M"])
+def test_unsupported_td64_unit_raises(unit):
+    # GH 52806
+    with pytest.raises(
+        ValueError, match=f"cannot construct a Timedelta from a unit {unit}"
+    ):
+        Timedelta(np.timedelta64(1, unit))
+
+
 def test_huge_nanoseconds_overflow():
     # GH 32402
     assert delta_to_nanoseconds(Timedelta(1e10)) == 1e10